This commit is contained in:
David Remer 2020-04-02 08:38:00 +03:00
parent ba97d7143d
commit 779eb31e4e
53 changed files with 8475 additions and 0 deletions

2
.gitignore vendored
View file

@ -1,4 +1,6 @@
/vendor/
/modules/
node_modules
phpunit.xml
.phpunit.result.cache
composer.lock

View file

@ -11,7 +11,33 @@
"role": "Company"
}
],
"repositories": [
{
"type": "path",
"url": "modules.local/ppcp-button"
},
{
"type": "path",
"url": "modules.local/ppcp-api-client"
},
{
"type": "path",
"url": "modules.local/ppcp-wc-gateway"
},
{
"type": "path",
"url": "modules.local/ppcp-session"
}
],
"require": {
"dhii/module-interface": "0.2.x-dev",
"psr/container": "^1.0",
"inpsyde/ppcp-button": "dev-master",
"inpsyde/ppcp-wc-gateway": "dev-master",
"oomphinc/composer-installers-extender": "^1.1",
"container-interop/service-provider": "^0.4.0",
"dhii/containers": "dev-develop",
"dhii/wp-containers": "^0.1.0@alpha"
},
"require-dev": {
"inpsyde/php-coding-standards": "@stable"
@ -27,5 +53,20 @@
"ci": [
"vendor/bin/phpcs"
]
},
"extra": {
"installer-types": [
"inpsyde-module"
],
"installer-paths": {
"modules/{$name}/": [
"type:inpsyde-module"
]
},
"hooks": {
"pre-commit": [
"vendor/bin/phpcbf"
]
}
}
}

View file

@ -0,0 +1,13 @@
{
"name": "inpsyde/ppcp-api-client",
"type": "inpsyde-module",
"require": {
"dhii/module-interface": "0.2.x-dev",
"inpsyde/ppcp-session": "dev-master"
},
"autoload": {
"psr-4": {
"Inpsyde\\PayPalCommerce\\ApiClient\\": "src/"
}
}
}

View file

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\ApiClient;
return [
];

View file

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\ApiClient;
use Dhii\Modular\Module\ModuleInterface;
return function (): ModuleInterface {
return new ApiModule();
};

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\ApiClient;
use Dhii\Data\Container\ContainerInterface;
use Inpsyde\PayPalCommerce\ApiClient\Authentication\Bearer;
use Inpsyde\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use Inpsyde\PayPalCommerce\ApiClient\Factory\LineItemFactory;
use Inpsyde\PayPalCommerce\ApiClient\Repository\CartRepository;
return [
'api.host' => function(ContainerInterface $container) : string {
return 'https://api.sandbox.paypal.com';
},
'api.key' => function(ContainerInterface $container) : string {
return 'AQB97CzMsd58-It1vxbcDAGvMuXNCXRD9le_XUaMlHB_U7XsU9IiItBwGQOtZv9sEeD6xs2vlIrL4NiD';
},
'api.secret' => function(ContainerInterface $container) : string {
return 'EILGMYK_0iiSbja8hT-nCBGl0BvKxEB4riHgyEO7QWDeUzCJ5r42JUEvrI7gpGyw0Qww8AIXxSdCIAny';
},
'api.bearer' => function(ContainerInterface $container) : Bearer {
return new Bearer(
$container->get('api.host'),
$container->get('api.key'),
$container->get('api.secret')
);
},
'api.endpoint.order' => function(ContainerInterface $container) : OrderEndpoint {
$sessionHandler = $container->get('session.handler');
return new OrderEndpoint(
$container->get('api.host'),
$container->get('api.bearer'),
$sessionHandler
);
},
'api.cart-repository' => function(ContainerInterface $container) : CartRepository {
$cart = WC()->cart;
$factory = $container->get('api.line-item-factory');
return new CartRepository($cart, $factory);
},
'api.line-item-factory' => function(ContainerInterface $container) : LineItemFactory {
return new LineItemFactory();
},
];

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\ApiClient;
use Dhii\Container\ServiceProvider;
use Dhii\Modular\Module\Exception\ModuleExceptionInterface;
use Dhii\Modular\Module\ModuleInterface;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
class ApiModule implements ModuleInterface
{
public function setup(): ServiceProviderInterface
{
return new ServiceProvider(
require __DIR__.'/../services.php',
require __DIR__.'/../extensions.php'
);
}
public function run(ContainerInterface $c)
{
// TODO: Implement run() method.
}
}

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\ApiClient\Authentication;
use Inpsyde\PayPalCommerce\ApiClient\Exception\RuntimeException;
class Bearer
{
private const CACHE_KEY = 'ppcp-bearer';
private $host;
private $key;
private $secret;
public function __construct(string $host, string $key, string $secret)
{
$this->host = $host;
$this->key = $key;
$this->secret = $secret;
}
public function bearer() : string
{
//ToDo: Do not store with wp_cache_get but as transient.
$bearer = wp_cache_get(self::CACHE_KEY);
if ( ! $bearer) {
return $this->newBearer();
}
return (string) $bearer;
}
public function newBearer() : string
{
$url = trailingslashit($this->host) . 'v1/oauth2/token?grant_type=client_credentials';
$args = [
'headers' => [
'Authorization' => 'Basic ' . base64_encode($this->key . ':' . $this->secret),
],
];
$response = wp_remote_post(
$url,
$args
);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
throw new RuntimeException(__('Could not create token.', 'woocommerce-paypal-commerce-gateway'));
}
$json = json_decode($response['body']);
if (! isset($json->access_token) || ! isset($json->expires_in)) {
throw new RuntimeException(__('Could not find token.', 'woocommerce-paypal-commerce-gateway'));
}
$token = (string) $json->access_token;
wp_cache_set(self::CACHE_KEY, $token, $json->expires_in);
return $token;
}
}

View file

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\ApiClient\Endpoint;
use Inpsyde\PayPalCommerce\ApiClient\Authentication\Bearer;
use Inpsyde\PayPalCommerce\ApiClient\Entity\LineItem;
use Inpsyde\PayPalCommerce\ApiClient\Entity\Order;
use Inpsyde\PayPalCommerce\ApiClient\Exception\RuntimeException;
use Inpsyde\PayPalCommerce\Session\SessionHandler;
class OrderEndpoint
{
private $host;
private $bearer;
private $sessionHandler;
public function __construct(string $host, Bearer $bearer, SessionHandler $sessionHandler)
{
$this->host = $host;
$this->bearer = $bearer;
$this->sessionHandler = $sessionHandler;
}
public function createForLineItems(LineItem ...$items) : ?Order {
$bearer = $this->bearer->bearer();
$data = [
'intent' => 'CAPTURE',
'purchase_units' => array_map(
function(LineItem $item) : array {
return $item->toArray();
},
$items
),
];
$url = trailingslashit($this->host) . 'v2/checkout/orders';
$args = [
'headers' => [
'Authorization' => 'Bearer ' . $bearer,
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
],
'body' => json_encode($data),
];
$response = wp_remote_post($url, $args);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 201) {
throw new RuntimeException(__('Could not create order.', 'woocommerce-paypal-commerce-gateway'));
}
$json = json_decode($response['body']);
$order = new Order($json);
$this->sessionHandler->setOrder($order);
return $order;
}
public function capture(Order $order) : Order
{
$bearer = $this->bearer->bearer();
$url = trailingslashit($this->host) . 'v2/checkout/orders/' . $order->id() . '/capture';
$args = [
'headers' => [
'Authorization' => 'Bearer ' . $bearer,
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
]
];
$response = wp_remote_post($url, $args);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 201) {
throw new RuntimeException(__('Could not capture order.', 'woocommerce-paypal-commerce-gateway'));
}
$json = json_decode($response['body']);
$order = new Order($json);
$this->sessionHandler->setOrder($order);
return $order;
}
}

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\ApiClient\Entity;
class LineItem
{
private $data;
public function __construct(\stdClass $data)
{
$this->data = $data;
}
public function referenceId() : int {
return $this->data->reference_id;
}
public function description() : string {
return $this->data->description;
}
public function quantity() : float {
return $this->data->quantity;
}
public function totalAmount() : float {
return $this->data->total_amount;
}
public function currencyCode() : string {
return $this->data->currency_code;
}
public function toArray() : array {
return [
'reference_id' => $this->referenceId(),
'description' => $this->quantity() . '× ' . $this->description(),
'amount' => [
'value' => $this->totalAmount(),
'currency_code' => $this->currencyCode(),
]
];
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\ApiClient\Entity;
class Order
{
private $data;
/**
* Order constructor.
*
* @param \stdClass $data $data is formed like the PayPal create order response.
*
* @see https://developer.paypal.com/docs/api/orders/v2/#orders-create-response
*/
public function __construct(\stdClass $data)
{
$this->data = $data;
}
public function id() : string {
return $this->data->id;
}
public function isApproved() : bool {
return $this->data->status === 'APPROVED';
}
public function isCompleted() : bool {
return $this->data->status === 'COMPLETED';
}
public function isCreated() : bool {
return $this->data->status === 'CREATED';
}
public function toArray() : array {
return (array) $this->data;
}
}

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\ApiClient\Exception;
class RuntimeException extends \RuntimeException
{
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\ApiClient\Factory;
use Inpsyde\PayPalCommerce\ApiClient\Entity\LineItem;
class LineItemFactory
{
public function fromWoocommerceLineItem(array $lineItem, string $currencyCode) : ?LineItem {
if (! $this->validateWoocommerceLineItem($lineItem)) {
return null;
}
$product = $lineItem['data'];
/**
* @var \WC_Product $product
*/
$data = (object) [
'reference_id' => $product->get_id(),
'quantity' => $lineItem['quantity'],
'description' => $product->get_name(),
'currency_code' => $currencyCode,
'total_amount' => $lineItem['line_subtotal'],
];
return new LineItem($data);
}
private function validateWoocommerceLineItem(array $lineItem) : bool {
$validate = [
'data' => function($value) : bool {
return is_a($value, \WC_Product::class);
},
'quantity' => 'is_numeric',
'line_subtotal' => 'is_numeric',
];
foreach ($validate as $key => $validator) {
if (! isset($lineItem[$key]) || ! $validator($lineItem[$key])) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\ApiClient\Repository;
use Inpsyde\PayPalCommerce\ApiClient\Entity\LineItem;
use Inpsyde\PayPalCommerce\ApiClient\Factory\LineItemFactory;
class CartRepository implements LineItemRepositoryInterface
{
private $cart;
private $factory;
public function __construct(\WC_Cart $cart, LineItemFactory $factory)
{
$this->cart = $cart;
$this->factory = $factory;
}
/**
* Returns all LineItem of the Woocommerce cart.
*
* @return array
*/
public function all() : array {
return array_map(
function(array $lineItem) : LineItem {
return $this->factory->fromWoocommerceLineItem($lineItem, get_woocommerce_currency());
},
array_values($this->cart->get_cart())
);
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\ApiClient\Repository;
use Inpsyde\PayPalCommerce\ApiClient\Entity\LineItem;
interface LineItemRepositoryInterface
{
/**
* @return LineItem[]
*/
public function all() : array;
}

1
modules.local/ppcp-button/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/node_modules/

View file

@ -0,0 +1,414 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./resources/js/button.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./resources/js/button.js":
/*!********************************!*\
!*** ./resources/js/button.js ***!
\********************************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _modules_Renderer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./modules/Renderer */ "./resources/js/modules/Renderer.js");
/* harmony import */ var _modules_SingleProductConfig__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./modules/SingleProductConfig */ "./resources/js/modules/SingleProductConfig.js");
/* harmony import */ var _modules_UpdateCart__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./modules/UpdateCart */ "./resources/js/modules/UpdateCart.js");
/* harmony import */ var _modules_ErrorHandler__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./modules/ErrorHandler */ "./resources/js/modules/ErrorHandler.js");
document.addEventListener('DOMContentLoaded', () => {
if (!typeof PayPalCommerceGateway) {
console.error('PayPal button could not be configured.');
return;
}
if (!document.querySelector(PayPalCommerceGateway.button.wrapper)) {
console.error('No wrapper for PayPal button found.');
return;
}
const context = PayPalCommerceGateway.context;
if (context === 'product' && !document.querySelector('form.cart')) {
return;
}
const errorHandler = new _modules_ErrorHandler__WEBPACK_IMPORTED_MODULE_3__["default"]();
const renderer = new _modules_Renderer__WEBPACK_IMPORTED_MODULE_0__["default"]({
url: PayPalCommerceGateway.button.url,
wrapper: PayPalCommerceGateway.button.wrapper
});
const updateCart = new _modules_UpdateCart__WEBPACK_IMPORTED_MODULE_2__["default"](PayPalCommerceGateway.ajax.change_cart.endpoint, PayPalCommerceGateway.ajax.change_cart.nonce);
let configurator = null;
if (context === 'product') {
configurator = new _modules_SingleProductConfig__WEBPACK_IMPORTED_MODULE_1__["default"](PayPalCommerceGateway, updateCart, renderer.showButtons.bind(renderer), renderer.hideButtons.bind(renderer), document.querySelector('form.cart'), errorHandler);
}
if (!configurator) {
console.error('No context for button found.');
return;
}
renderer.render(configurator.configuration());
});
/***/ }),
/***/ "./resources/js/modules/ButtonsToggleListener.js":
/*!*******************************************************!*\
!*** ./resources/js/modules/ButtonsToggleListener.js ***!
\*******************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/**
* When you can't add something to the cart, the PayPal buttons should not show.
* Therefore we listen for changes on the add to cart button and show/hide the buttons accordingly.
*/
class ButtonsToggleListener {
constructor(element, showCallback, hideCallback) {
this.element = element;
this.showCallback = showCallback;
this.hideCallback = hideCallback;
this.observer = null;
}
init() {
const config = { attributes: true };
const callback = () => {
if (this.element.classList.contains('disabled')) {
this.hideCallback();
return;
}
this.showCallback();
};
this.observer = new MutationObserver(callback);
this.observer.observe(this.element, config);
}
disconnect() {
this.observer.disconnect();
}
}
/* harmony default export */ __webpack_exports__["default"] = (ButtonsToggleListener);
/***/ }),
/***/ "./resources/js/modules/ErrorHandler.js":
/*!**********************************************!*\
!*** ./resources/js/modules/ErrorHandler.js ***!
\**********************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
class ErrorHandler {
constructor() {
this.wrapper = document.querySelector('.woocommerce-notices-wrapper');
}
message(text) {
this.wrapper.classList.add('woocommerce-error');
this.wrapper.innerText = this.sanitize(text);
}
sanitize(text) {
const textarea = document.createElement('textarea');
textarea.innerHTML = text;
return textarea.value;
}
clear() {
if (!this.wrapper.classList.contains('woocommerce-error')) {
return;
}
this.wrapper.classList.remove('woocommerce-error');
this.wrapper.innerText = '';
}
}
/* harmony default export */ __webpack_exports__["default"] = (ErrorHandler);
/***/ }),
/***/ "./resources/js/modules/Renderer.js":
/*!******************************************!*\
!*** ./resources/js/modules/Renderer.js ***!
\******************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
class Renderer {
constructor(config) {
this.config = config;
}
render(buttonConfig) {
const script = document.createElement('script');
if (typeof paypal !== 'object') {
script.setAttribute('src', this.config.url);
script.addEventListener('load', event => {
this.renderButtons(buttonConfig);
});
document.body.append(script);
return;
}
this.renderButtons(buttonConfig);
}
renderButtons(buttonConfig) {
paypal.Buttons(buttonConfig).render(this.config.wrapper);
}
hideButtons() {
document.querySelector(this.config.wrapper).style.display = 'none';
}
showButtons() {
document.querySelector(this.config.wrapper).style.display = 'block';
}
}
/* harmony default export */ __webpack_exports__["default"] = (Renderer);
/***/ }),
/***/ "./resources/js/modules/SingleProductConfig.js":
/*!*****************************************************!*\
!*** ./resources/js/modules/SingleProductConfig.js ***!
\*****************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _ButtonsToggleListener__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./ButtonsToggleListener */ "./resources/js/modules/ButtonsToggleListener.js");
class SingleProductConfig {
constructor(config, updateCart, showButtonCallback, hideButtonCallback, formElement, errorHandler) {
this.config = config;
this.updateCart = updateCart;
this.showButtonCallback = showButtonCallback;
this.hideButtonCallback = hideButtonCallback;
this.formElement = formElement;
this.errorHandler = errorHandler;
}
configuration() {
if (this.hasVariations()) {
const observer = new _ButtonsToggleListener__WEBPACK_IMPORTED_MODULE_0__["default"](this.formElement.querySelector('.single_add_to_cart_button'), this.showButtonCallback, this.hideButtonCallback);
observer.init();
}
const onApprove = (data, actions) => {
return actions.redirect(this.config.redirect);
};
return {
createOrder: this.createOrder(),
onApprove,
onError: error => {
this.errorHandler.message(error);
}
};
}
createOrder() {
const createOrder = (data, actions) => {
this.errorHandler.clear();
const product = document.querySelector('[name="add-to-cart"]').value;
const qty = document.querySelector('[name="quantity"]').value;
const variations = this.variations();
const onResolve = purchase_units => {
return fetch(this.config.ajax.create_order.endpoint, {
method: 'POST',
body: JSON.stringify({
nonce: this.config.ajax.create_order.nonce,
purchase_units
})
}).then(function (res) {
return res.json();
}).then(function (data) {
if (!data.success) {
//Todo: Error handling
return;
}
return data.data.id;
});
};
const promise = this.updateCart.update(onResolve, product, qty, variations);
return promise;
};
return createOrder;
}
variations() {
if (!this.hasVariations()) {
return null;
}
const attributes = [...this.formElement.querySelectorAll("[name^='attribute_']")].map(element => {
return {
value: element.value,
name: element.name
};
});
return attributes;
}
hasVariations() {
return this.formElement.classList.contains('variations_form');
}
}
/* harmony default export */ __webpack_exports__["default"] = (SingleProductConfig);
/***/ }),
/***/ "./resources/js/modules/UpdateCart.js":
/*!********************************************!*\
!*** ./resources/js/modules/UpdateCart.js ***!
\********************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
class UpdateCart {
constructor(endpoint, nonce) {
this.endpoint = endpoint;
this.nonce = nonce;
}
update(onResolve, product, qty, variations) {
return new Promise((resolve, reject) => {
fetch(this.endpoint, {
method: 'POST',
body: JSON.stringify({
nonce: this.nonce,
product,
qty,
variations
})
}).then(result => {
return result.json();
}).then(result => {
if (!result.success) {
reject(result.data);
return;
}
const resolved = onResolve(result.data);
resolve(resolved);
});
});
}
}
/* harmony default export */ __webpack_exports__["default"] = (UpdateCart);
/***/ })
/******/ });
//# sourceMappingURL=button.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
{
"name": "inpsyde/ppcp-button",
"type": "inpsyde-module",
"require": {
"dhii/module-interface": "0.2.x-dev",
"inpsyde/ppcp-api-client": "dev-master"
},
"autoload": {
"psr-4": {
"Inpsyde\\PayPalCommerce\\Button\\": "src/"
}
}
}

View file

@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
return [
];

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Button;
use Dhii\Modular\Module\ModuleInterface;
return function (): ModuleInterface {
return new ButtonModule();
};

6344
modules.local/ppcp-button/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
{
"name": "ppc-button",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"main": "resources/js/button.js",
"devDependencies": {
"babel-core": "^6.25.0",
"babel-loader": "^7.1.1",
"babel-preset-env": "^1.6.0",
"cross-env": "^5.0.1",
"file-loader": "^4.2.0",
"node-sass": "^4.13.0",
"sass-loader": "^8.0.0",
"webpack": "^4.23.1",
"webpack-cli": "^3.1.2"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
"dev": "cross-env BABEL_ENV=default webpack --watch"
},
"dependencies": {}
}

View file

@ -0,0 +1,51 @@
import Renderer from './modules/Renderer';
import SingleProductConfig from './modules/SingleProductConfig';
import UpdateCart from './modules/UpdateCart';
import ErrorHandler from './modules/ErrorHandler';
document.addEventListener(
'DOMContentLoaded',
() => {
if (! typeof(PayPalCommerceGateway)) {
console.error('PayPal button could not be configured.');
return;
}
if (! document.querySelector(PayPalCommerceGateway.button.wrapper)) {
console.error('No wrapper for PayPal button found.');
return;
}
const context = PayPalCommerceGateway.context;
if (context === 'product' && ! document.querySelector('form.cart') ) {
return;
}
const errorHandler = new ErrorHandler();
const renderer = new Renderer({
url: PayPalCommerceGateway.button.url,
wrapper:PayPalCommerceGateway.button.wrapper
});
const updateCart = new UpdateCart(
PayPalCommerceGateway.ajax.change_cart.endpoint,
PayPalCommerceGateway.ajax.change_cart.nonce
);
let configurator = null;
if (context === 'product') {
configurator = new SingleProductConfig(
PayPalCommerceGateway,
updateCart,
renderer.showButtons.bind(renderer),
renderer.hideButtons.bind(renderer),
document.querySelector('form.cart'),
errorHandler
);
}
if (! configurator) {
console.error('No context for button found.');
return;
}
renderer.render(configurator.configuration());
}
);

View file

@ -0,0 +1,32 @@
/**
* When you can't add something to the cart, the PayPal buttons should not show.
* Therefore we listen for changes on the add to cart button and show/hide the buttons accordingly.
*/
class ButtonsToggleListener {
constructor(element, showCallback, hideCallback) {
this.element = element;
this.showCallback = showCallback;
this.hideCallback = hideCallback;
this.observer = null;
}
init() {
const config = { attributes : true };
const callback = () => {
if (this.element.classList.contains('disabled')) {
this.hideCallback();
return;
}
this.showCallback();
}
this.observer = new MutationObserver(callback);
this.observer.observe(this.element, config);
}
disconnect() {
this.observer.disconnect();
}
}
export default ButtonsToggleListener;

View file

@ -0,0 +1,27 @@
class ErrorHandler {
constructor() {
this.wrapper = document.querySelector('.woocommerce-notices-wrapper');
}
message(text) {
this.wrapper.classList.add('woocommerce-error');
this.wrapper.innerText = this.sanitize(text);
}
sanitize(text) {
const textarea = document.createElement('textarea');
textarea.innerHTML = text;
return textarea.value;
}
clear() {
if (! this.wrapper.classList.contains('woocommerce-error')) {
return;
}
this.wrapper.classList.remove('woocommerce-error');
this.wrapper.innerText = '';
}
}
export default ErrorHandler;

View file

@ -0,0 +1,39 @@
class Renderer {
constructor(config) {
this.config = config;
}
render(buttonConfig) {
const script = document.createElement('script');
if (typeof paypal !== 'object') {
script.setAttribute('src', this.config.url);
script.addEventListener('load', (event) => {
this.renderButtons(buttonConfig);
})
document.body.append(script);
return;
}
this.renderButtons(buttonConfig);
}
renderButtons(buttonConfig) {
paypal.Buttons(
buttonConfig
).render(this.config.wrapper);
}
hideButtons() {
document.querySelector(this.config.wrapper).style.display = 'none';
}
showButtons() {
document.querySelector(this.config.wrapper).style.display = 'block';
}
}
export default Renderer;

View file

@ -0,0 +1,95 @@
import ButtonsToggleListener from "./ButtonsToggleListener";
class SingleProductConfig {
constructor(
config,
updateCart,
showButtonCallback,
hideButtonCallback,
formElement,
errorHandler
) {
this.config = config;
this.updateCart = updateCart;
this.showButtonCallback = showButtonCallback;
this.hideButtonCallback = hideButtonCallback;
this.formElement = formElement;
this.errorHandler = errorHandler;
}
configuration() {
if ( this.hasVariations() ) {
const observer = new ButtonsToggleListener(
this.formElement.querySelector('.single_add_to_cart_button'),
this.showButtonCallback,
this.hideButtonCallback
);
observer.init();
}
const onApprove = (data, actions) => {
return actions.redirect(this.config.redirect);
}
return {
createOrder: this.createOrder(),
onApprove,
onError: (error) => {
this.errorHandler.message(error);
}
}
}
createOrder() {
const createOrder = (data, actions) => {
this.errorHandler.clear();
const product = document.querySelector('[name="add-to-cart"]').value;
const qty = document.querySelector('[name="quantity"]').value;
const variations = this.variations();
const onResolve = (purchase_units) => {
return fetch(this.config.ajax.create_order.endpoint, {
method: 'POST',
body: JSON.stringify({
nonce:this.config.ajax.create_order.nonce,
purchase_units
})
}).then(function(res) {
return res.json();
}).then(function(data) {
if (! data.success) {
//Todo: Error handling
return;
}
return data.data.id;
});
};
const promise = this.updateCart.update(onResolve, product, qty, variations);
return promise;
};
return createOrder;
}
variations() {
if (! this.hasVariations()) {
return null;
}
const attributes = [...this.formElement.querySelectorAll("[name^='attribute_']")].map(
(element) => {
return {
value:element.value,
name:element.name
}
}
);
return attributes;
}
hasVariations() {
return this.formElement.classList.contains('variations_form');
}
}
export default SingleProductConfig;

View file

@ -0,0 +1,39 @@
class UpdateCart {
constructor(endpoint, nonce) {
this.endpoint = endpoint;
this.nonce = nonce;
}
update(onResolve, product, qty, variations) {
return new Promise( (resolve, reject) => {
fetch(
this.endpoint,
{
method: 'POST',
body: JSON.stringify({
nonce: this.nonce,
product,
qty,
variations
})
}
).then(
(result) => {
return result.json();
}
).then( (result) => {
if (! result.success) {
reject(result.data);
return;
}
const resolved = onResolve(result.data);
resolve(resolved);
}
)
});
}
}
export default UpdateCart;

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Button;
use Inpsyde\PayPalCommerce\Button\Assets\SmartButton;
use Dhii\Data\Container\ContainerInterface;
use Inpsyde\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use Inpsyde\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use Inpsyde\PayPalCommerce\Button\Endpoint\RequestData;
use Inpsyde\PayPalCommerce\Button\Exception\RuntimeException;
return [
'button.smart-button' => function(ContainerInterface $container) {
$isSandbox = true;
return new SmartButton($container->get('button.url'), $isSandbox);
},
'button.url' => function(ContainerInterface $container) : string {
return plugins_url('/modules/ppcp-button/', dirname(__FILE__, 3) . '/woocommerce-paypal-commerce-gateway.php');
},
'button.request-data' => function(ContainerInterface $container) : RequestData {
return new RequestData();
},
'button.endpoint.change-cart' => function(ContainerInterface $container) : ChangeCartEndpoint {
if (! \WC()->cart) {
throw new RuntimeException('cant initialize endpoint at this moment');
}
$cart = WC()->cart;
$shipping = WC()->shipping();
$requestData = $container->get('button.request-data');
$repository = $container->get('api.cart-repository');
return new ChangeCartEndpoint($cart, $shipping, $requestData, $repository);
},
'button.endpoint.create-order' => function(ContainerInterface $container) : CreateOrderEndpoint {
$requestData = $container->get('button.request-data');
$repository = $container->get('api.cart-repository');
$apiClient = $container->get('api.endpoint.order');
return new CreateOrderEndpoint($requestData, $repository, $apiClient);
}
];

View file

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Button\Assets;
use Inpsyde\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use Inpsyde\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
class SmartButton
{
private $moduleUrl;
private $isSandbox;
public function __construct(
string $moduleUrl,
bool $isSandbox
) {
$this->moduleUrl = $moduleUrl;
$this->isSandbox = $isSandbox;
}
public function renderWrapper() : bool
{
$renderer = function() {
echo '<div id="ppc-button"></div>';
};
if (is_product()) {
add_action(
'woocommerce_single_product_summary',
$renderer,
31
);
return true;
}
if (is_checkout()) {
add_action(
'wp_footer',
$renderer,
31
);
return true;
}
return false;
}
public function enqueue() : bool
{
wp_enqueue_script(
'paypal-smart-button',
$this->moduleUrl . '/assets/js/button.js'
);
$params = [
'client-id' => 'AcVzowpNCpTxFzLG7onQI4JD0sVcA0BkZv-D42qRZPv_gZ8cNfX9zGL_8bXmSu7cbJ5B2DH7sot8vDpw',
];
$smartButtonUrl = add_query_arg($params, 'https://www.paypal.com/sdk/js');
$localize = [
'redirect' => wc_get_checkout_url(),
'context' => $this->context(),
'ajax' => [
'change_cart' => [
'endpoint' => home_url(\WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT )),
'nonce' => wp_create_nonce(ChangeCartEndpoint::nonce()),
],
'create_order' => [
'endpoint' => home_url(\WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT )),
'nonce' => wp_create_nonce(CreateOrderEndpoint::nonce()),
],
],
'button' => [
'wrapper' => '#ppc-button',
'url' =>$smartButtonUrl,
]
];
wp_localize_script(
'paypal-smart-button',
'PayPalCommerceGateway',
$localize
);
return true;
}
private function context() : string {
$context = 'mini-cart';
if (is_product()) {
$context = 'product';
}
if (is_cart()) {
$context = 'cart';
}
if (is_checkout()) {
$context = 'checkout';
}
return $context;
}
}

View file

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Button;
use Dhii\Container\ServiceProvider;
use Dhii\Modular\Module\ModuleInterface;
use Inpsyde\PayPalCommerce\Button\Assets\SmartButton;
use Inpsyde\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use Inpsyde\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use Inpsyde\PayPalCommerce\Button\Endpoint\RequestData;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
class ButtonModule implements ModuleInterface
{
public function setup(): ServiceProviderInterface
{
return new ServiceProvider(
require __DIR__.'/../services.php',
require __DIR__.'/../extensions.php'
);
}
/**
* @inheritDoc
*/
public function run(ContainerInterface $container)
{
$smartButton = $container->get('button.smart-button');
/**
* @var SmartButton $smartButton
*/
add_action(
'wp',
function() use ($smartButton) {
if (is_admin()) {
return;
}
$smartButton->renderWrapper();
}
);
add_action('wp_enqueue_scripts', function() use ($smartButton) {
$smartButton->enqueue();
});
add_action(
'wc_ajax_' . ChangeCartEndpoint::ENDPOINT,
function() use ($container) {
$endpoint = $container->get('button.endpoint.change-cart');
/**
* @var ChangeCartEndpoint $endpoint
*/
$endpoint->handleRequest();
}
);
add_action(
'wc_ajax_' . CreateOrderEndpoint::ENDPOINT,
function() use ($container) {
$endpoint = $container->get('button.endpoint.create-order');
/**
* @var ChangeCartEndpoint $endpoint
*/
$endpoint->handleRequest();
}
);
}
}

View file

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Button\Endpoint;
use Inpsyde\PayPalCommerce\ApiClient\Entity\LineItem;
use Inpsyde\PayPalCommerce\ApiClient\Repository\CartRepository;
use Inpsyde\PayPalCommerce\Button\Exception\RuntimeException;
class ChangeCartEndpoint implements EndpointInterface
{
const ENDPOINT = 'ppc-change-cart';
private $cart;
private $shipping;
private $requestData;
private $repository;
public function __construct(
\WC_Cart $cart,
\WC_Shipping $shipping,
RequestData $requestData,
CartRepository $repository
) {
$this->cart = $cart;
$this->shipping = $shipping;
$this->requestData = $requestData;
$this->repository = $repository;
}
public static function nonce() : string
{
return self::ENDPOINT . get_current_user_id();
}
public function handleRequest() : bool
{
try {
$data = $this->requestData->readRequest($this->nonce());
if (
! isset($data['product'])
|| ! isset($data['qty'])
) {
wp_send_json_error(__('Necessary fields not defined. Action aborted.', 'woocommerce-paypal-commerce-gateway'));
return false;
}
$product = wc_get_product((int) $data['product']);
if (! $product) {
wp_send_json_error(__('No product defined. Action aborted.', 'woocommerce-paypal-commerce-gateway'));
return false;
}
$quantity = (int) $data['qty'];
$this->shipping->reset_shipping();
$this->cart->empty_cart(false);
$success = (! $product->is_type('variable')) ?
$success = $this->addProduct($product, $quantity)
: $this->addVariableProduct($product, $quantity, $data['variations']);
if (! $success) {
$message = __('Something went wrong. Action aborted', 'woocommerce-paypal-commerce-gateway');
$errors = wc_get_notices('error');
if (count($errors)) {
$message = array_reduce(
$errors,
function(string $add, $error) : string {
return $add . $error['notice'] . ' ';
},
''
);
wc_clear_notices();
}
wp_send_json_error($message);
return $success;
}
wp_send_json_success($this->generatePurchaseUnits());
return $success;
} catch (RuntimeException $error) {
wp_send_json_error($error->getMessage());
return false;
}
}
private function addProduct(\WC_Product $product, int $quantity) : bool {
return false !== $this->cart->add_to_cart($product->get_id(), $quantity);
}
private function addVariableProduct(\WC_Product $product, int $quantity, array $postVariations) : bool {
foreach ($postVariations as $key => $value) {
$variations[$value['name']] = $value['value'];
}
$dataStore = \WC_Data_Store::load( 'product' );
$variationId = $dataStore->find_matching_product_variation( $product, $variations );
//ToDo: Check stock status for variation.
return false !== WC()->cart->add_to_cart( $product->get_id(), $quantity, $variationId, $variations );
}
private function generatePurchaseUnits() : array {
/**
* ToDo: Somewhere we need to add shipping costs. Total costs can change later on, but we should
* get a good estimate, about the total amount.
**/
/**
* ToDo: Currently, although an array, only one purchase_unit is supported!
*
* @see https://developer.paypal.com/docs/api/orders/v2/#orders-create-request-body
*/
return array_map(
function(LineItem $lineItem) : array {
return $lineItem->toArray();
},
$this->repository->all()
);
}
}

View file

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Button\Endpoint;
use Inpsyde\PayPalCommerce\Button\Exception\RuntimeException;
use Inpsyde\PayPalCommerce\ApiClient\Repository\CartRepository;
use Inpsyde\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
class CreateOrderEndpoint implements EndpointInterface
{
const ENDPOINT = 'ppc-create-order';
private $requestData;
private $repository;
private $apiEndpoint;
public function __construct(
RequestData $requestData,
CartRepository $repository,
OrderEndpoint $apiEndpoint
) {
$this->requestData = $requestData;
$this->repository = $repository;
$this->apiEndpoint = $apiEndpoint;
}
public static function nonce() : string
{
return self::ENDPOINT . get_current_user_id();
}
public function handleRequest() : bool
{
try {
$this->requestData->readRequest($this->nonce());
$lineItems = $this->repository->all();
$order = $this->apiEndpoint->createForLineItems(
...$lineItems
);
wp_send_json_success($order->toArray());
return false;
} catch (RuntimeException $error ) {
wp_send_json_error($error->getMessage());
return false;
}
}
}

View file

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Button\Endpoint;
interface EndpointInterface
{
public static function nonce() : string;
public function handleRequest() : bool;
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Button\Endpoint;
use Inpsyde\PayPalCommerce\Button\Exception\RuntimeException;
class RequestData
{
public function readRequest(string $nonce) : array
{
$stream = file_get_contents('php://input');
$json = json_decode($stream, true);
if (
! isset($json['nonce'])
|| !wp_verify_nonce($json['nonce'], $nonce)
) {
throw new RuntimeException(
__('Could not validate nonce.', 'woocommerce-paypal-commerce-gateway')
);
}
return $this->sanitize($json);
}
private function sanitize(array $assocArray) : array {
$data = [];
foreach ((array) $assocArray as $rawKey => $rawValue) {
if (! is_array($rawValue)) {
$data[sanitize_text_field($rawKey)] = sanitize_text_field($rawValue);
continue;
}
$data[sanitize_text_field($rawKey)] = $this->sanitize($rawValue);
}
return $data;
}
}

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Button\Exception;
class RuntimeException extends \RuntimeException
{
}

View file

@ -0,0 +1,35 @@
const path = require( 'path' );
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
devtool: 'sourcemap',
mode: isProduction ? 'production' : 'development',
target: 'web',
entry: {
button: path.resolve( './resources/js/button.js' ),
},
output: {
path: path.resolve( __dirname, 'assets/' ),
filename: 'js/[name].js',
},
module: {
rules: [{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
{
loader: 'file-loader',
options: {
name: 'css/[name].css',
}
},
{loader:'sass-loader'}
]
}]
}
};

View file

@ -0,0 +1,13 @@
{
"name": "inpsyde/ppcp-session",
"type": "inpsyde-module",
"require": {
"dhii/module-interface": "0.2.x-dev",
"inpsyde/ppcp-api-client": "dev-master"
},
"autoload": {
"psr-4": {
"Inpsyde\\PayPalCommerce\\Session\\": "src/"
}
}
}

View file

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Session;
return [
];

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Session;
use Dhii\Modular\Module\ModuleInterface;
return function (): ModuleInterface {
return new SessionModule();
};

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Session;
use Dhii\Data\Container\ContainerInterface;
return [
'session.handler' => function(ContainerInterface $container) : SessionHandler {
if (is_admin()) {
return new SessionHandler();
}
$result = WC()->session->get(SessionHandler::ID);
if (is_a($result, SessionHandler::class)) {
return $result;
}
$sessionHandler = new SessionHandler();
WC()->session->set(SessionHandler::ID, $sessionHandler);
return $sessionHandler;
}
];

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Session;
use Inpsyde\PayPalCommerce\ApiClient\Entity\Order;
class SessionHandler
{
const ID = 'ppcp';
private $order;
public function order() : ?Order {
return $this->order;
}
public function setOrder(Order $order) : SessionHandler {
$this->order = $order;
$this->storeSession();
return $this;
}
private function storeSession() {
WC()->session->set(self::ID, $this);
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Session;
use Dhii\Container\ServiceProvider;
use Dhii\Modular\Module\ModuleInterface;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
class SessionModule implements ModuleInterface
{
public function setup(): ServiceProviderInterface
{
return new ServiceProvider(
require __DIR__.'/../services.php',
require __DIR__.'/../extensions.php'
);
}
public function run(ContainerInterface $c)
{
add_action(
'woocommerce_init',
function() use ($c) {
$c->get('session.handler');
}
);
}
}

View file

@ -0,0 +1,14 @@
{
"name": "inpsyde/ppcp-wc-gateway",
"type": "inpsyde-module",
"require": {
"dhii/module-interface": "0.2.x-dev",
"inpsyde/ppcp-session": "dev-master",
"inpsyde/ppcp-api-client": "dev-master"
},
"autoload": {
"psr-4": {
"Inpsyde\\PayPalCommerce\\WcGateway\\": "src/"
}
}
}

View file

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway;
return [
];

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway;
use Dhii\Modular\Module\ModuleInterface;
return function (): ModuleInterface {
return new WcGatewayModule();
};

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway;
use Dhii\Data\Container\ContainerInterface;
use Inpsyde\PayPalCommerce\WcGateway\Checkout\DisableGateways;
use Inpsyde\PayPalCommerce\WcGateway\Gateway\WcGateway;
return [
'wcgateway.gateway' => function(ContainerInterface $container) : WcGateway {
$sessionHandler = $container->get('session.handler');
$endpoint = $container->get('api.endpoint.order');
return new WcGateway($sessionHandler, $endpoint);
},
'wcgateway.disabler' => function(ContainerInterface $container) : DisableGateways {
$sessionHandler = $container->get('session.handler');
return new DisableGateways($sessionHandler);
}
];

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway\Checkout;
use Inpsyde\PayPalCommerce\Session\SessionHandler;
use Inpsyde\PayPalCommerce\WcGateway\Gateway\WcGateway;
class DisableGateways
{
private $sessionHandler;
public function __construct(SessionHandler $sessionHandler)
{
$this->sessionHandler = $sessionHandler;
}
public function handler(array $methods) : array
{
if (! $this->needsToDisableGateways()) {
return $methods;
}
return [WcGateway::ID => $methods[WcGateway::ID]];
}
private function needsToDisableGateways() : bool
{
return $this->sessionHandler->order() !== null;
}
}

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway\Gateway;
use Inpsyde\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use Inpsyde\PayPalCommerce\Session\SessionHandler;
class WcGateway extends \WC_Payment_Gateway
{
const ID = 'ppcp-gateway';
private $isSandbox = true;
private $sessionHandler;
private $endpoint;
public function __construct(SessionHandler $sessionHandler, OrderEndpoint $endpoint)
{
$this->sessionHandler = $sessionHandler;
$this->endpoint = $endpoint;
$this->id = self::ID;
$this->method_title = __('PayPal Payments', 'woocommerce-paypal-gateway');
$this->method_description = __('Provide your customers with the PayPal payment system', 'woocommerce-paypal-gateway');
$this->init_form_fields();
$this->init_settings();
$this->isSandbox = $this->get_option( 'sandbox_on', 'yes' ) === 'yes';
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
}
public function init_form_fields()
{
$this->form_fields = [
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-gateway' ),
'type' => 'checkbox',
'label' => __( 'Enable PayPal Payments', 'woocommerce-paypal-gateway' ),
'default' => 'yes'
),
'sandbox_on' => array(
'title' => __( 'Enable Sandbox', 'woocommerce-paypal-gateway' ),
'type' => 'checkbox',
'label' => __( 'For testing your integration, you can enable the sandbox.', 'woocommerce-paypal-gateway' ),
'default' => 'yes'
),
];
}
function process_payment( $order_id ) : ?array
{
global $woocommerce;
$wcOrder = new \WC_Order( $order_id );
//ToDo: We need to fetch the order from paypal again to get it with the new status.
$order = $this->sessionHandler->order();
$errorMessage = null;
if (! $order || ! $order->isApproved()) {
$errorMessage = 'not approve yet';
}
$errorMessage = null;
if ($errorMessage) {
wc_add_notice( __('Payment error:', 'woocommerce-paypal-gateway') . $errorMessage, 'error' );
return null;
}
/**
* ToDo: If shipping or something else changes, we need to patch the order!
* We should also handle the shipping address and update if needed
* @see https://developer.paypal.com/docs/api/orders/v2/#orders_patch
**/
$order = $this->endpoint->capture($order);
$wcOrder->update_status('on-hold', __( 'Awaiting payment.', 'woocommerce-paypal-gateway' ));
if ($order->isCompleted()) {
$wcOrder->update_status('processing', __( 'Payment received.', 'woocommerce-paypal-gateway' ));
}
$woocommerce->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $wcOrder )
);
}
}

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway;
use Dhii\Container\ServiceProvider;
use Dhii\Modular\Module\Exception\ModuleExceptionInterface;
use Dhii\Modular\Module\ModuleInterface;
use Inpsyde\PayPalCommerce\WcGateway\Checkout\DisableGateways;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
class WcGatewayModule implements ModuleInterface
{
public function setup(): ServiceProviderInterface
{
return new ServiceProvider(
require __DIR__.'/../services.php',
require __DIR__.'/../extensions.php'
);
}
public function run(ContainerInterface $c)
{
add_filter(
'woocommerce_payment_gateways',
function($methods) use ($c) : array {
$methods[] = $c->get('wcgateway.gateway');
return (array) $methods;
}
);
add_filter(
'woocommerce_available_payment_gateways',
function($methods) use ($c) : array {
$disabler = $c->get('wcgateway.disabler');
/**
* @var DisableGateways $disabler
*/
return $disabler->handler((array) $methods);
}
);
}
}

25
src/PluginModule.php Normal file
View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce;
use Dhii\Container\ServiceProvider;
use Dhii\Modular\Module\Exception\ModuleExceptionInterface;
use Dhii\Modular\Module\ModuleInterface;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
class PluginModule implements ModuleInterface
{
public function setup(): ServiceProviderInterface
{
return new ServiceProvider([], []);
}
public function run(ContainerInterface $c)
{
// TODO: Implement run() method.
}
}

View file

@ -15,3 +15,63 @@ declare( strict_types = 1 );
*/
namespace Inpsyde\PayPalCommerce;
use Dhii\Container\CachingContainer;
use Dhii\Container\CompositeCachingServiceProvider;
use Dhii\Container\DelegatingContainer;
use Dhii\Container\ProxyContainer;
use Dhii\Modular\Module\ModuleInterface;
(function () {
if (!class_exists(CompositeCachingServiceProvider::class)
&& file_exists(__DIR__.'/vendor/autoload.php')
) {
include_once __DIR__.'/vendor/autoload.php';
}
function init()
{
static $initialized;
if (!$initialized) {
$modules = [new PluginModule(__DIR__)];
foreach (glob(plugin_dir_path(__FILE__).'modules/*/module.php') as $moduleFile) {
$modules[] = (@require $moduleFile)();
}
$providers = [];
foreach ($modules as $module) {
/* @var $module ModuleInterface */
$providers[] = $module->setup();
}
$proxy = new ProxyContainer();
$provider = new CompositeCachingServiceProvider($providers);
$container = new CachingContainer(new DelegatingContainer($provider));
$proxy->setInnerContainer($container);
foreach ($modules as $module) {
/* @var $module ModuleInterface */
$module->run($container);
}
$initialized = true;
}
}
add_action(
'plugins_loaded',
function () {
init();
}
);
register_activation_hook(
__FILE__,
function () {
init();
do_action('woocommerce-paypal-commerce-gateway.activate');
}
);
register_deactivation_hook(
__FILE__,
function () {
init();
do_action('woocommerce-paypal-commerce-gateway.deactivate');
}
);
})();