diff --git a/.env b/.env
index 4a2decb..4e23cff 100644
--- a/.env
+++ b/.env
@@ -6,3 +6,8 @@ CLIENT_SECRET_SANDBOX=
SANDBOX=
MERCHANT_ID=
+
+###> nelmio/cors-bundle ###
+TRUSTED_PROXIES=127.0.0.1,localhost,REMOTE_ADDR
+CORS_ALLOW_ORIGIN='^https?://(.*)(localhost|127\.0\.0\.1)(:[0-9]+)?$'
+###< nelmio/cors-bundle ###
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 606eed1..ce399b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,8 @@ devenv.local.nix
# PhpStorm
/.idea/
+
+
+# js
+node_modules/
+bundle/Resources/app/storefront/dist/*
\ No newline at end of file
diff --git a/bundle/Resources/app/storefront/build/webpack.config.js b/bundle/Resources/app/storefront/build/webpack.config.js
new file mode 100644
index 0000000..e0df20c
--- /dev/null
+++ b/bundle/Resources/app/storefront/build/webpack.config.js
@@ -0,0 +1,13 @@
+const { join, resolve } = require('path');
+
+module.exports = () => {
+ return {
+ resolve: {
+ alias: {
+ '@paypal': resolve(
+ join(__dirname, '..', 'node_modules', '@paypal'),
+ ),
+ },
+ },
+ };
+};
diff --git a/bundle/Resources/app/storefront/package-lock.json b/bundle/Resources/app/storefront/package-lock.json
new file mode 100644
index 0000000..e444b1f
--- /dev/null
+++ b/bundle/Resources/app/storefront/package-lock.json
@@ -0,0 +1,44 @@
+{
+ "name": "swag-paypal-storefront",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "swag-paypal-storefront",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@paypal/paypal-js": "~8"
+ }
+ },
+ "node_modules/@paypal/paypal-js": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@paypal/paypal-js/-/paypal-js-8.0.0.tgz",
+ "integrity": "sha512-wiABljZw5IbvuWkuspmRA/xN9T78zXbEfBzz6Ea94Nxg5hoxs1l9u4rzjO30IEglmIFH3+fLsGla+PfnUzvFSw==",
+ "dependencies": {
+ "promise-polyfill": "^8.3.0"
+ }
+ },
+ "node_modules/promise-polyfill": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
+ "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg=="
+ }
+ },
+ "dependencies": {
+ "@paypal/paypal-js": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@paypal/paypal-js/-/paypal-js-8.0.0.tgz",
+ "integrity": "sha512-wiABljZw5IbvuWkuspmRA/xN9T78zXbEfBzz6Ea94Nxg5hoxs1l9u4rzjO30IEglmIFH3+fLsGla+PfnUzvFSw==",
+ "requires": {
+ "promise-polyfill": "^8.3.0"
+ }
+ },
+ "promise-polyfill": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
+ "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg=="
+ }
+ }
+}
diff --git a/bundle/Resources/app/storefront/package.json b/bundle/Resources/app/storefront/package.json
new file mode 100644
index 0000000..5d75ed8
--- /dev/null
+++ b/bundle/Resources/app/storefront/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "swag-paypal-storefront",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Shopware Storefront PayPal",
+ "license": "MIT",
+ "dependencies": {
+ "@paypal/paypal-js": "~8"
+ }
+}
diff --git a/bundle/Resources/app/storefront/src/checkout/swag-paypal-app.wallet.js b/bundle/Resources/app/storefront/src/checkout/swag-paypal-app.wallet.js
new file mode 100644
index 0000000..1bcdd51
--- /dev/null
+++ b/bundle/Resources/app/storefront/src/checkout/swag-paypal-app.wallet.js
@@ -0,0 +1,170 @@
+import DomAccess from 'src/helper/dom-access.helper';
+import FormSerializeUtil from 'src/utility/form/form-serialize.util';
+import AppClient from 'src/service/app-client.service';
+import PageLoadingIndicatorUtil from 'src/utility/loading-indicator/page-loading-indicator.util';
+import Plugin from 'src/plugin-system/plugin.class';
+import {loadScript} from '@paypal/paypal-js';
+
+const BASE_URL = 'http://localhost:8080/storefront/';
+
+export default class SwagPaypalAppWallet extends Plugin {
+ static options = {
+ /**
+ * Options for the PayPal script
+ */
+ languageIso: 'en_GB',
+ currency: 'EUR',
+ intent: 'capture',
+ commit: true,
+ clientId: '',
+ merchantPayerId: '',
+
+ /**
+ * Is set, if the plugin is used on the order edit page
+ */
+ orderId: null,
+
+ /**
+ * Usage inside the plugin
+ */
+ confirmOrderFormSelector: '#confirmOrderForm',
+ confirmOrderButtonSelector: 'button[type="submit"]',
+
+ /**
+ * In a productive app, we should filter this to only values that we need for PayPal order creation
+ */
+ cart: {},
+ salesChannelContext: {},
+ };
+
+ init() {
+ this._client = new AppClient('SCDPayPalApp');
+
+ this.createButton();
+ }
+
+ async createButton() {
+ await this.fetchClientConfig();
+ const paypal = await this.createScript();
+ this.renderButton(paypal);
+ }
+
+ createScript() {
+ return loadScript(this.getScriptOptions());
+ }
+
+ renderButton(paypal) {
+ this.confirmOrderForm = DomAccess.querySelector(document, this.options.confirmOrderFormSelector);
+
+ DomAccess.querySelector(this.confirmOrderForm, this.options.confirmOrderButtonSelector).classList.add('d-none');
+
+ return paypal.Buttons(this.getButtonConfig()).render(this.el);
+ }
+
+
+ /**
+ * @return {Object}
+ */
+ getScriptOptions() {
+ return {
+ components: 'buttons,messages',
+ 'client-id': this.options.clientId,
+ commit: !!this.options.commit,
+ locale: this.options.languageIso,
+ currency: this.options.currency,
+ intent: this.options.intent,
+ 'enable-funding': 'paylater,venmo',
+ 'merchant-id': this.options.merchantPayerId,
+ };
+ }
+
+ getButtonConfig() {
+ return {
+ style: {
+ size: 'small',
+ shape: 'rect',
+ color: 'gold',
+ label: 'pay',
+ },
+
+ /**
+ * Will be called if when the payment process starts
+ */
+ createOrder: this.createOrder.bind(this),
+
+ /**
+ * Will be called if the payment process is approved by paypal
+ */
+ onApprove: this.onApprove.bind(this),
+
+ /**
+ * Check form validity & show loading spinner on confirm click
+ */
+ onClick: this.onClick.bind(this),
+
+ /**
+ * Will be called if an error occurs during the payment process.
+ */
+ onError: this.onError.bind(this),
+ };
+ }
+
+ /**
+ * @return {Promise}
+ */
+ async createOrder() {
+ if (!this.confirmOrderForm.checkValidity()) {
+ throw new Error('Checkout form not valid');
+ }
+
+ const data = {
+ formData: FormSerializeUtil.serializeJson(this.confirmOrderForm),
+ cart: this.options.cart,
+ salesChannelContext: this.options.salesChannelContext,
+ orderId: this.options.orderId,
+ }
+
+ const response = await this._client.post(
+ `${BASE_URL}order/`,
+ {
+ body: JSON.stringify(data),
+ }
+ ).then((response) => response.json());
+
+ return response.orderId;
+ }
+
+ onApprove(data) {
+ PageLoadingIndicatorUtil.create();
+
+ const input = document.createElement('input');
+ input.setAttribute('type', 'hidden');
+ input.setAttribute('name', 'paypalOrderId');
+ input.setAttribute('value', data.orderID);
+
+ this.confirmOrderForm.appendChild(input);
+ this.confirmOrderForm.submit();
+ }
+
+ onClick(data, actions) {
+ if (!this.confirmOrderForm.checkValidity()) {
+ return actions.reject();
+ }
+
+ return actions.resolve();
+ }
+
+ onError(error) {
+ console.error(error);
+ window.location.reload();
+ }
+
+ async fetchClientConfig() {
+ const response = await this._client.get(`${BASE_URL}config/`);
+
+ this.options = {
+ ...this.options,
+ ...await response.json(),
+ };
+ }
+}
diff --git a/bundle/Resources/app/storefront/src/main.js b/bundle/Resources/app/storefront/src/main.js
new file mode 100644
index 0000000..c75d0f2
--- /dev/null
+++ b/bundle/Resources/app/storefront/src/main.js
@@ -0,0 +1,7 @@
+// Register them via the existing PluginManager
+const PluginManager = window.PluginManager;
+PluginManager.register(
+ 'SwagPaypalAppWallet',
+ () => import('./checkout/swag-paypal-app.wallet'),
+ '[data-swag-paypal-app-wallet]',
+);
diff --git a/bundle/Resources/views/storefront/page/checkout/confirm/index.html.twig b/bundle/Resources/views/storefront/page/checkout/confirm/index.html.twig
new file mode 100644
index 0000000..cbd031d
--- /dev/null
+++ b/bundle/Resources/views/storefront/page/checkout/confirm/index.html.twig
@@ -0,0 +1,19 @@
+{% sw_extends '@Storefront/storefront/page/checkout/confirm/index.html.twig' %}
+
+{% block page_checkout_confirm_form_submit %}
+ {{ parent() }}
+
+ {% block page_checkout_confirm_form_submit_paypal_app_spb %}
+
+ {% if (context.paymentMethod.technicalName === 'payment_SCDPayPalApp_paypal') and page.order is not defined %}
+
+
+ {% endif %}
+
+ {% endblock %}
+{% endblock %}
diff --git a/bundle/manifest.xml b/bundle/manifest.xml
index 012e99d..2ea5d25 100644
--- a/bundle/manifest.xml
+++ b/bundle/manifest.xml
@@ -5,18 +5,24 @@
SCDPayPalApp
-
-
shopware AG
(c) by shopware AG
- 0.0.2
- Resources/config/plugin.png
+ 0.0.3
MIT
http://localhost:8080/lifecycle/register
devsecret
+
+ sales_channel
+ customer
+ currency
+ country
+ language
+ payment_method
+ shipping_method
+
diff --git a/composer.json b/composer.json
index 393bd4b..0039267 100644
--- a/composer.json
+++ b/composer.json
@@ -8,6 +8,7 @@
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/doctrine-migrations-bundle": "^3.3",
+ "nelmio/cors-bundle": "^2.3",
"shopware/app-bundle": "^2.0",
"symfony/console": "7.0.*",
"symfony/dotenv": "7.0.*",
@@ -28,6 +29,15 @@
},
"sort-packages": true
},
+ "repositories": [
+ {
+ "type": "path",
+ "url": "../app*",
+ "options": {
+ "symlink": true
+ }
+ }
+ ],
"autoload": {
"psr-4": {
"Swag\\PayPalApp\\": "src/"
@@ -68,5 +78,9 @@
"allow-contrib": false,
"require": "7.0.*"
}
+ },
+ "require-dev": {
+ "symfony/debug-bundle": "7.0.*",
+ "symfony/web-profiler-bundle": "7.0.*"
}
}
diff --git a/composer.lock b/composer.lock
index 2e12e7a..3d53c65 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": "15d75d194390171d29eb0a1afcaf63e4",
+ "content-hash": "7d4f4949ea726801ccdee3ed10ff7df4",
"packages": [
{
"name": "doctrine/cache",
@@ -1567,6 +1567,68 @@
],
"time": "2023-10-27T15:32:31+00:00"
},
+ {
+ "name": "nelmio/cors-bundle",
+ "version": "2.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nelmio/NelmioCorsBundle.git",
+ "reference": "78fcdb91f76b080a1008133def9c7f613833933d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/78fcdb91f76b080a1008133def9c7f613833933d",
+ "reference": "78fcdb91f76b080a1008133def9c7f613833933d",
+ "shasum": ""
+ },
+ "require": {
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
+ "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.3.6",
+ "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Nelmio\\CorsBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nelmio",
+ "homepage": "http://nelm.io"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors"
+ }
+ ],
+ "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application",
+ "keywords": [
+ "api",
+ "cors",
+ "crossdomain"
+ ],
+ "support": {
+ "issues": "https://github.com/nelmio/NelmioCorsBundle/issues",
+ "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.4.0"
+ },
+ "time": "2023-11-30T16:41:19+00:00"
+ },
{
"name": "nikic/php-parser",
"version": "v5.0.2",
@@ -2299,17 +2361,11 @@
},
{
"name": "shopware/app-php-sdk",
- "version": "2.0.3",
- "source": {
- "type": "git",
- "url": "https://github.com/shopware/app-php-sdk.git",
- "reference": "62250ca75f58add692ef523e1b6e90dde28b66dd"
- },
+ "version": "2.99.99",
"dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/shopware/app-php-sdk/zipball/62250ca75f58add692ef523e1b6e90dde28b66dd",
- "reference": "62250ca75f58add692ef523e1b6e90dde28b66dd",
- "shasum": ""
+ "type": "path",
+ "url": "../app-php-sdk",
+ "reference": "9be771166087643aa887cf4a1ab9fe4b0ce0dddf"
},
"require": {
"lcobucci/clock": "^3",
@@ -2340,7 +2396,21 @@
"Shopware\\App\\SDK\\": "src/"
}
},
- "notification-url": "https://packagist.org/downloads/",
+ "autoload-dev": {
+ "psr-4": {
+ "Shopware\\App\\SDK\\Tests\\": "tests/"
+ }
+ },
+ "scripts": {
+ "test": [
+ "phpunit"
+ ],
+ "check": [
+ "phpunit",
+ "php-cs-fixer fix",
+ "phpstan analyse"
+ ]
+ },
"license": [
"MIT"
],
@@ -2351,14 +2421,16 @@
"shopware"
],
"support": {
- "chat": "https://slack.shopware.com",
- "docs": "https://developer.shopware.com",
- "forum": "https://forum.shopware.com",
"issues": "https://issues.shopware.com",
- "source": "https://github.com/shopware/app-php-sdk/tree/2.0.3",
- "wiki": "https://developer.shopware.com"
+ "forum": "https://forum.shopware.com",
+ "wiki": "https://developer.shopware.com",
+ "docs": "https://developer.shopware.com",
+ "chat": "https://slack.shopware.com"
},
- "time": "2023-12-09T10:22:41+00:00"
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
},
{
"name": "symfony/cache",
@@ -3957,16 +4029,16 @@
},
{
"name": "symfony/maker-bundle",
- "version": "v1.57.0",
+ "version": "v1.58.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/maker-bundle.git",
- "reference": "2c90181911241648356b828b86b04fe3571aca0b"
+ "reference": "c4f8d2c5d55950e1a49e822efc83a8511bee8a36"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/2c90181911241648356b828b86b04fe3571aca0b",
- "reference": "2c90181911241648356b828b86b04fe3571aca0b",
+ "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/c4f8d2c5d55950e1a49e822efc83a8511bee8a36",
+ "reference": "c4f8d2c5d55950e1a49e822efc83a8511bee8a36",
"shasum": ""
},
"require": {
@@ -4029,7 +4101,7 @@
],
"support": {
"issues": "https://github.com/symfony/maker-bundle/issues",
- "source": "https://github.com/symfony/maker-bundle/tree/v1.57.0"
+ "source": "https://github.com/symfony/maker-bundle/tree/v1.58.0"
},
"funding": [
{
@@ -4045,7 +4117,7 @@
"type": "tidelift"
}
],
- "time": "2024-03-22T12:00:21+00:00"
+ "time": "2024-04-06T15:08:12+00:00"
},
{
"name": "symfony/monolog-bridge",
@@ -5463,7 +5535,505 @@
"time": "2024-03-23T06:35:46+00:00"
}
],
- "packages-dev": [],
+ "packages-dev": [
+ {
+ "name": "symfony/debug-bundle",
+ "version": "v7.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/debug-bundle.git",
+ "reference": "b0db5c443883ce5c10c2265c77feb9833c3d9d6d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/b0db5c443883ce5c10c2265c77feb9833c3d9d6d",
+ "reference": "b0db5c443883ce5c10c2265c77feb9833c3d9d6d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-xml": "*",
+ "php": ">=8.2",
+ "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/twig-bridge": "^6.4|^7.0",
+ "symfony/var-dumper": "^6.4|^7.0"
+ },
+ "conflict": {
+ "symfony/config": "<6.4",
+ "symfony/dependency-injection": "<6.4"
+ },
+ "require-dev": {
+ "symfony/config": "^6.4|^7.0",
+ "symfony/web-profiler-bundle": "^6.4|^7.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\DebugBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/debug-bundle/tree/v7.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-01-23T15:02:46+00:00"
+ },
+ {
+ "name": "symfony/translation-contracts",
+ "version": "v3.4.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation-contracts.git",
+ "reference": "43810bdb2ddb5400e5c5e778e27b210a0ca83b6b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/43810bdb2ddb5400e5c5e778e27b210a0ca83b6b",
+ "reference": "43810bdb2ddb5400e5c5e778e27b210a0ca83b6b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.4-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to translation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/translation-contracts/tree/v3.4.2"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-01-23T14:51:35+00:00"
+ },
+ {
+ "name": "symfony/twig-bridge",
+ "version": "v7.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/twig-bridge.git",
+ "reference": "1d5745dac2e043553177a3b88a76b99c2a2f6c2e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/1d5745dac2e043553177a3b88a76b99c2a2f6c2e",
+ "reference": "1d5745dac2e043553177a3b88a76b99c2a2f6c2e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/translation-contracts": "^2.5|^3",
+ "twig/twig": "^3.0.4"
+ },
+ "conflict": {
+ "phpdocumentor/reflection-docblock": "<3.2.2",
+ "phpdocumentor/type-resolver": "<1.4.0",
+ "symfony/console": "<6.4",
+ "symfony/form": "<6.4",
+ "symfony/http-foundation": "<6.4",
+ "symfony/http-kernel": "<6.4",
+ "symfony/mime": "<6.4",
+ "symfony/serializer": "<6.4",
+ "symfony/translation": "<6.4",
+ "symfony/workflow": "<6.4"
+ },
+ "require-dev": {
+ "egulias/email-validator": "^2.1.10|^3|^4",
+ "league/html-to-markdown": "^5.0",
+ "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
+ "symfony/asset": "^6.4|^7.0",
+ "symfony/asset-mapper": "^6.4|^7.0",
+ "symfony/console": "^6.4|^7.0",
+ "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/expression-language": "^6.4|^7.0",
+ "symfony/finder": "^6.4|^7.0",
+ "symfony/form": "^6.4|^7.0",
+ "symfony/html-sanitizer": "^6.4|^7.0",
+ "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/intl": "^6.4|^7.0",
+ "symfony/mime": "^6.4|^7.0",
+ "symfony/polyfill-intl-icu": "~1.0",
+ "symfony/property-info": "^6.4|^7.0",
+ "symfony/routing": "^6.4|^7.0",
+ "symfony/security-acl": "^2.8|^3.0",
+ "symfony/security-core": "^6.4|^7.0",
+ "symfony/security-csrf": "^6.4|^7.0",
+ "symfony/security-http": "^6.4|^7.0",
+ "symfony/serializer": "^6.4.3|^7.0.3",
+ "symfony/stopwatch": "^6.4|^7.0",
+ "symfony/translation": "^6.4|^7.0",
+ "symfony/web-link": "^6.4|^7.0",
+ "symfony/workflow": "^6.4|^7.0",
+ "symfony/yaml": "^6.4|^7.0",
+ "twig/cssinliner-extra": "^2.12|^3",
+ "twig/inky-extra": "^2.12|^3",
+ "twig/markdown-extra": "^2.12|^3"
+ },
+ "type": "symfony-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bridge\\Twig\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides integration for Twig with various Symfony components",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/twig-bridge/tree/v7.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-03-28T21:02:11+00:00"
+ },
+ {
+ "name": "symfony/twig-bundle",
+ "version": "v7.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/twig-bundle.git",
+ "reference": "acab2368f53491e018bf31ef48b39df55a6812ef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/acab2368f53491e018bf31ef48b39df55a6812ef",
+ "reference": "acab2368f53491e018bf31ef48b39df55a6812ef",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": ">=2.1",
+ "php": ">=8.2",
+ "symfony/config": "^6.4|^7.0",
+ "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/twig-bridge": "^6.4|^7.0",
+ "twig/twig": "^3.0.4"
+ },
+ "conflict": {
+ "symfony/framework-bundle": "<6.4",
+ "symfony/translation": "<6.4"
+ },
+ "require-dev": {
+ "symfony/asset": "^6.4|^7.0",
+ "symfony/expression-language": "^6.4|^7.0",
+ "symfony/finder": "^6.4|^7.0",
+ "symfony/form": "^6.4|^7.0",
+ "symfony/framework-bundle": "^6.4|^7.0",
+ "symfony/routing": "^6.4|^7.0",
+ "symfony/stopwatch": "^6.4|^7.0",
+ "symfony/translation": "^6.4|^7.0",
+ "symfony/web-link": "^6.4|^7.0",
+ "symfony/yaml": "^6.4|^7.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\TwigBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a tight integration of Twig into the Symfony full-stack framework",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/twig-bundle/tree/v7.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-02-15T11:33:06+00:00"
+ },
+ {
+ "name": "symfony/web-profiler-bundle",
+ "version": "v7.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/web-profiler-bundle.git",
+ "reference": "542daea1345fe181cbfd52db00717174a838ea0a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/542daea1345fe181cbfd52db00717174a838ea0a",
+ "reference": "542daea1345fe181cbfd52db00717174a838ea0a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/config": "^6.4|^7.0",
+ "symfony/framework-bundle": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/routing": "^6.4|^7.0",
+ "symfony/twig-bundle": "^6.4|^7.0",
+ "twig/twig": "^3.0.4"
+ },
+ "conflict": {
+ "symfony/form": "<6.4",
+ "symfony/mailer": "<6.4",
+ "symfony/messenger": "<6.4"
+ },
+ "require-dev": {
+ "symfony/browser-kit": "^6.4|^7.0",
+ "symfony/console": "^6.4|^7.0",
+ "symfony/css-selector": "^6.4|^7.0",
+ "symfony/stopwatch": "^6.4|^7.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\WebProfilerBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a development tool that gives detailed information about the execution of any request",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "dev"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/web-profiler-bundle/tree/v7.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-02-22T20:27:20+00:00"
+ },
+ {
+ "name": "twig/twig",
+ "version": "v3.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/Twig.git",
+ "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
+ "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-mbstring": "^1.3",
+ "symfony/polyfill-php80": "^1.22"
+ },
+ "require-dev": {
+ "psr/container": "^1.0|^2.0",
+ "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Twig\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Twig Team",
+ "role": "Contributors"
+ },
+ {
+ "name": "Armin Ronacher",
+ "email": "armin.ronacher@active-4.com",
+ "role": "Project Founder"
+ }
+ ],
+ "description": "Twig, the flexible, fast, and secure template language for PHP",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "templating"
+ ],
+ "support": {
+ "issues": "https://github.com/twigphp/Twig/issues",
+ "source": "https://github.com/twigphp/Twig/tree/v3.8.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-11-21T18:54:41+00:00"
+ }
+ ],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
diff --git a/config/bundles.php b/config/bundles.php
index 5173c31..c2f01ed 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -10,4 +10,5 @@
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
+ Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
];
diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml
index 877eb25..4756e36 100644
--- a/config/packages/framework.yaml
+++ b/config/packages/framework.yaml
@@ -9,6 +9,9 @@ framework:
#esi: true
#fragments: true
+ trusted_proxies: '%env(TRUSTED_PROXIES)%'
+ trusted_headers: [ 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix', 'forwarded' ]
+
when@test:
framework:
test: true
diff --git a/config/packages/nelmio_cors.yaml b/config/packages/nelmio_cors.yaml
new file mode 100644
index 0000000..8437198
--- /dev/null
+++ b/config/packages/nelmio_cors.yaml
@@ -0,0 +1,8 @@
+nelmio_cors:
+ defaults:
+ origin_regex: true
+ allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
+ allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
+ allow_headers: ['Content-Type', 'Authorization', 'shopware-app-shop-id', 'shopware-app-token']
+ expose_headers: ['Link']
+ max_age: 3600
diff --git a/src/Api/Client/ClientFactory.php b/src/Api/Client/ClientFactory.php
index 0105e3f..78bc425 100644
--- a/src/Api/Client/ClientFactory.php
+++ b/src/Api/Client/ClientFactory.php
@@ -3,6 +3,7 @@
namespace Swag\PayPalApp\Api\Client;
use Swag\PayPalApp\Api\Constants;
+use Swag\PayPalApp\Repository\CredentialsRepository;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\HttpOptions;
@@ -12,24 +13,21 @@ class ClientFactory
{
public function __construct(
private readonly AuthenticationBuilder $authenticationBuilder,
- #[Autowire(env: 'MERCHANT_ID')]
- private readonly string $merchantId,
- #[Autowire(env: 'SANDBOX')]
- private readonly bool $sandbox,
+ private readonly CredentialsRepository $credentialsRepository,
)
{
}
public function createFirstPartyClient(ApiContext $context): HttpClientInterface
{
- $shopConfig = $this->getShopConfig($context);
+ $shopConfig = $this->credentialsRepository->getShopConfig($context);
return HttpClient::create($this->getDefaultHttpOptions($shopConfig)->toArray());
}
public function createThirdPartyClient(ApiContext $context): HttpClientInterface
{
- $shopConfig = $this->getShopConfig($context);
+ $shopConfig = $this->credentialsRepository->getShopConfig($context);
$httpOptions = $this->getDefaultHttpOptions($shopConfig);
$httpOptions->setHeaders([
'PayPal-Auth-Assertion' => $this->authenticationBuilder->getAuthAssertion($shopConfig->isSandbox(), $shopConfig->getMerchantId()),
@@ -39,7 +37,6 @@ public function createThirdPartyClient(ApiContext $context): HttpClientInterface
return HttpClient::create($httpOptions->toArray());
}
-
private function getDefaultHttpOptions(CredentialsIdentifier $shopConfig): HttpOptions
{
$httpOptions = new HttpOptions();
@@ -48,11 +45,4 @@ private function getDefaultHttpOptions(CredentialsIdentifier $shopConfig): HttpO
return $httpOptions;
}
-
- private function getShopConfig(ApiContext $context): CredentialsIdentifier
- {
- // TODO: build a repository to do this
-
- return new CredentialsIdentifier($this->merchantId, $this->sandbox);
- }
}
\ No newline at end of file
diff --git a/src/Api/Client/CredentialsIdentifier.php b/src/Api/Client/CredentialsIdentifier.php
index b073774..8eba378 100644
--- a/src/Api/Client/CredentialsIdentifier.php
+++ b/src/Api/Client/CredentialsIdentifier.php
@@ -5,12 +5,18 @@
class CredentialsIdentifier
{
public function __construct(
+ private readonly string $clientId,
private readonly ?string $merchantId,
private readonly bool $sandbox,
)
{
}
+ public function getClientId(): string
+ {
+ return $this->clientId;
+ }
+
public function getMerchantId(): ?string
{
return $this->merchantId;
diff --git a/src/Api/Converter/Util/PurchaseUnitProvider.php b/src/Api/Converter/Util/PurchaseUnitProvider.php
index 9c7553c..4db6bc9 100644
--- a/src/Api/Converter/Util/PurchaseUnitProvider.php
+++ b/src/Api/Converter/Util/PurchaseUnitProvider.php
@@ -78,7 +78,8 @@ public function createPurchaseUnit(
private function createShipping(?Customer $customer, ?ShopwareOrder $order): ?Shipping
{
- $shippingAddress = \current($order?->getDeliveries() ?? [])?->getShippingOrderAddress() ?? $customer?->getActiveShippingAddress();
+ $delivery = \current($order?->getDeliveries() ?? []);
+ $shippingAddress = $delivery ? $delivery->getShippingOrderAddress() : $customer?->getActiveShippingAddress();
if ($shippingAddress === null) {
return null;
}
diff --git a/src/Api/Gateway/OrderGateway.php b/src/Api/Gateway/OrderGateway.php
index a8f9df2..c85e92e 100644
--- a/src/Api/Gateway/OrderGateway.php
+++ b/src/Api/Gateway/OrderGateway.php
@@ -54,9 +54,9 @@ public function captureOrder(string $orderId, ApiContext $context): Order
);
}
- public function patchOrder(string $orderId, PatchCollection $patches, ApiContext $context): Order
+ public function patchOrder(string $orderId, PatchCollection $patches, ApiContext $context): void
{
- return $this->request(
+ $this->request(
'PATCH',
self::GATEWAY_URL . '/' . $orderId,
$patches,
diff --git a/src/Controller/Payment/PayPalController.php b/src/Controller/Payment/PayPalController.php
index dca076c..c78938d 100644
--- a/src/Controller/Payment/PayPalController.php
+++ b/src/Controller/Payment/PayPalController.php
@@ -14,7 +14,9 @@
use Swag\PayPalApp\Api\Converter\PayPalOrderBuilder;
use Swag\PayPalApp\Api\Gateway\OrderGateway;
use Swag\PayPalApp\Api\Struct\V2\Common\Link;
+use Swag\PayPalApp\Api\Struct\V2\PatchCollection;
use Swag\PayPalApp\Service\OrderExecuteService;
+use Swag\PayPalApp\Service\PatchBuilder;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
@@ -28,12 +30,25 @@ public function __construct(
private readonly OrderGateway $orderGateway,
private readonly ResponseSigner $responseSigner,
private readonly OrderExecuteService $orderExecuteService,
+ private readonly PatchBuilder $patchBuilder,
) {
}
#[Route('/pay', name: 'payment.paypal.pay', methods: ['POST'])]
public function pay(PaymentPayAction $payAction, ShopInterface $shop): ResponseInterface
{
+ $orderId = $payAction->requestData['paypalOrderId'];
+ if ($orderId ?? null) {
+ $apiContext = new ApiContext($payAction->order->getSalesChannelId(), $shop, preferRepresentation: true);
+
+ $patch = $this->patchBuilder->createPurchaseUnitPatch($payAction->order, $payAction->orderTransaction);
+ $this->orderGateway->patchOrder($orderId, new PatchCollection([$patch]), $apiContext);
+
+ $response = PaymentResponse::redirect($payAction->returnUrl . '&' . \http_build_query(['token' => $orderId]));
+
+ return $this->responseSigner->signResponse($response, $shop);
+ }
+
$order = $this->orderBuilder->getOrder($payAction, new ParameterBag($payAction->requestData));
$apiContext = new ApiContext($payAction->order->getSalesChannelId(), $shop, preferRepresentation: true);
diff --git a/src/Controller/Storefront/ConfigController.php b/src/Controller/Storefront/ConfigController.php
new file mode 100644
index 0000000..a5b8a42
--- /dev/null
+++ b/src/Controller/Storefront/ConfigController.php
@@ -0,0 +1,48 @@
+claims->getSalesChannelId(),
+ $action->shop,
+ );
+ $credentials = $this->credentialsRepository->getShopConfig($apiContext);
+
+ return new JsonResponse([
+ 'clientId' => $credentials->getClientId(),
+ 'merchantPayerId' => $credentials->getMerchantId(),
+ ]);
+ }
+}
diff --git a/src/Controller/Storefront/OrderController.php b/src/Controller/Storefront/OrderController.php
new file mode 100644
index 0000000..eded10d
--- /dev/null
+++ b/src/Controller/Storefront/OrderController.php
@@ -0,0 +1,47 @@
+getContent(), true, 512, JSON_THROW_ON_ERROR);
+ $cart = new Cart($body['cart']);
+ $salesChannelContext = new SalesChannelContext($body['salesChannelContext']);
+ $formData = new ParameterBag($body['formData'] ?? []);
+
+ $order = $this->orderBuilder->getOrderFromCart($cart, $salesChannelContext, $formData);
+
+ $apiContext = new ApiContext($action->claims->getSalesChannelId(), $action->shop, preferRepresentation: true);
+ $order = $this->orderGateway->createOrder($order, $apiContext);
+
+ return new JsonResponse([
+ 'orderId' => $order->getId(),
+ ]);
+ }
+}
diff --git a/src/Repository/CredentialsRepository.php b/src/Repository/CredentialsRepository.php
new file mode 100644
index 0000000..afb1af7
--- /dev/null
+++ b/src/Repository/CredentialsRepository.php
@@ -0,0 +1,33 @@
+sandbox ? $this->clientIdSandbox : $this->clientId, $this->merchantId, $this->sandbox);
+ }
+}
\ No newline at end of file
diff --git a/src/Service/PatchBuilder.php b/src/Service/PatchBuilder.php
new file mode 100644
index 0000000..933a359
--- /dev/null
+++ b/src/Service/PatchBuilder.php
@@ -0,0 +1,53 @@
+
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Swag\PayPalApp\Service;
+
+use Shopware\App\SDK\Context\Cart\CartPrice;
+use Shopware\App\SDK\Context\Order\Order;
+use Shopware\App\SDK\Context\Order\OrderTransaction;
+use Swag\PayPalApp\Api\Converter\Util\ItemListProvider;
+use Swag\PayPalApp\Api\Converter\Util\PurchaseUnitProvider;
+use Swag\PayPalApp\Api\Struct\V2\Patch;
+
+class PatchBuilder
+{
+ /**
+ * @internal
+ */
+ public function __construct(
+ private readonly PurchaseUnitProvider $purchaseUnitProvider,
+ private readonly ItemListProvider $itemListProvider,
+ ) {
+ }
+
+ public function createPurchaseUnitPatch(
+ Order $order,
+ OrderTransaction $orderTransaction,
+ ): Patch {
+ $purchaseUnit = $this->purchaseUnitProvider->createPurchaseUnit(
+ $orderTransaction->getAmount(),
+ $order->getShippingCosts(),
+ null,
+ $this->itemListProvider->getItemList($order->getCurrency(), $order),
+ $order->getCurrency(),
+ $order->getTaxStatus() !== CartPrice::TAX_STATE_GROSS, /* @phpstan-ignore-line */
+ $order,
+ $orderTransaction
+ );
+ $purchaseUnitArray = \json_decode((string) \json_encode($purchaseUnit), true);
+
+ $purchaseUnitPatch = new Patch();
+ $purchaseUnitPatch->assign([
+ 'op' => Patch::OPERATION_REPLACE,
+ 'path' => '/purchase_units/@reference_id==\'default\'',
+ ]);
+ $purchaseUnitPatch->setValue($purchaseUnitArray);
+
+ return $purchaseUnitPatch;
+ }
+}
diff --git a/symfony.lock b/symfony.lock
index 606b526..93fba2f 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -26,6 +26,18 @@
"migrations/.gitignore"
]
},
+ "nelmio/cors-bundle": {
+ "version": "2.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.5",
+ "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c"
+ },
+ "files": [
+ "config/packages/nelmio_cors.yaml"
+ ]
+ },
"php-http/discovery": {
"version": "1.19",
"recipe": {