diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 925dbd0a5..e376013c3 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -18,8 +18,9 @@ jobs: env: # Change these values depending on project. ps-module-name: saferpayofficial - project-workspace-dir: ${{ github.workspace }}/../workspace + container-name: ps-lint strategy: + fail-fast: false matrix: PS: [ 1.7.8-7.4 ] testsuite: [ lint, phpstan ] @@ -33,59 +34,28 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Set Swap Space - uses: Invertus/set-swap-space@master - with: - swap-size-gb: 10 - - - name: Set up workspace - shell: bash - run: | - eval `ssh-agent -s` - ssh-add - <<< "${{ secrets.PS_MODULE_WORKSPACE_SAFERPAY_PRIVATE_KEY }}" - git clone git@github.com:Invertus/ps-module-workspace.git ${{ env.project-workspace-dir }} - cd ${{ env.project-workspace-dir }} - sed 's/PROJECT_NAME=.*/PROJECT_NAME=${{ env.ps-module-name }}/g' < .env.dist | - sed 's/PS_VERSION_TAG=.*/PS_VERSION_TAG=${{ matrix.PS }}/g' > .env - make create-modules-dir - cp -R ${{ github.workspace }} ${{ env.project-workspace-dir }}/prestashop/${{ matrix.PS }}/modules/${{ env.ps-module-name }} - - - name: Run PrestaShop - run: | - (cd ${{ env.project-workspace-dir }} && docker compose up -d) - - - name: Healthcheck + # lint and phpstan need the PrestaShop sources on disk and the matching PHP + # version, but no running shop, so the public image is started idle with the + # checked-out module mounted in place. + - name: Start PrestaShop container run: | - timeout 120s sh -c 'until docker ps | grep ${{ env.ps-module-name }}-ps-prestashop-${{ matrix.PS }} | grep -q "(healthy)"; do echo "Waiting for container to be healthy..."; sleep 1; done' - - - name: Cache composer folder - uses: actions/cache@v3 - with: - path: ${{ env.project-workspace-dir }}/prestashop/${{ matrix.PS }}/modules/${{ env.ps-module-name }}/vendor - key: ${{ github.sha }} - - - name: Install git - run: | - docker exec -i ${{ env.ps-module-name }}-ps-prestashop-${{ matrix.PS }} bash -c "apt update && apt install -y git" - - - name: Remove old module - run: docker exec -i ${{ env.ps-module-name }}-ps-prestashop-${{ matrix.PS }} bash -c "cd /var/www/html/modules && rm -rf ${{ env.ps-module-name }}" - - - name: Install module - run: docker exec -i ${{ env.ps-module-name }}-ps-prestashop-${{ matrix.PS }} bash -c "cd /var/www/html/modules && git clone https://github.com/Invertus/${{ env.ps-module-name }}" + docker run -d --name ${{ env.container-name }} \ + -v ${{ github.workspace }}:/var/www/html/modules/${{ env.ps-module-name }} \ + --entrypoint tail prestashop/prestashop:${{ matrix.PS }} -f /dev/null - - name: Switch to the correct branch + - name: Install git and composer in container run: | - docker exec -i ${{ env.ps-module-name }}-ps-prestashop-${{ matrix.PS }} bash -c "cd /var/www/html/modules/${{ env.ps-module-name }} && git fetch origin && git checkout ${{ github.head_ref || github.ref_name }}" + docker exec -i ${{ env.container-name }} bash -c "apt-get update -qq && apt-get install -y -qq git" + docker exec -i ${{ env.container-name }} bash -c "php -r \"copy('https://getcomposer.org/installer', '/tmp/composer-setup.php');\" && php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer --quiet && composer --version" - name: Install composer dependencies - run: docker exec -i ${{ env.ps-module-name }}-ps-prestashop-${{ matrix.PS }} bash -c "cd /var/www/html/modules/${{ env.ps-module-name }} && composer install" + run: docker exec -i ${{ env.container-name }} bash -c "cd /var/www/html/modules/${{ env.ps-module-name }} && composer install --no-interaction" - name: PHP version - run: docker exec -i ${{ env.ps-module-name }}-ps-prestashop-${{ matrix.PS }} bash -c "php -v" + run: docker exec -i ${{ env.container-name }} bash -c "php -v" - name: Run ${{ matrix.testsuite }} tests - run: docker exec -i ${{ env.ps-module-name }}-ps-prestashop-${{ matrix.PS }} bash -c "cd /var/www/html/modules/${{ env.ps-module-name }} && make ci-${{ matrix.testsuite }} ps_version_tag=${{ matrix.PS }}" + run: docker exec -i ${{ env.container-name }} bash -c "cd /var/www/html/modules/${{ env.ps-module-name }} && make ci-${{ matrix.testsuite }} ps_version_tag=${{ matrix.PS }}" prepare-zip: name: Prepare module ZIP artifact diff --git a/.github/workflows/create_zip.yml b/.github/workflows/create_zip.yml index bc651db1e..70f1bc32e 100644 --- a/.github/workflows/create_zip.yml +++ b/.github/workflows/create_zip.yml @@ -20,6 +20,25 @@ jobs: with: fetch-depth: 0 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: views/js/admin/settings-app/pnpm-lock.yaml + + - name: Build React settings app + run: | + cd views/js/admin/settings-app + pnpm install --frozen-lockfile + pnpm run build + rm -rf node_modules src + - name: Build module ZIP run: | composer install --no-dev --optimize-autoloader --classmap-authoritative diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7a27648a4..ba9fc0cb2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,6 +18,25 @@ jobs: with: php-version: '5.6' + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: views/js/admin/settings-app/pnpm-lock.yaml + + - name: Build React settings app + run: | + cd views/js/admin/settings-app + pnpm install --frozen-lockfile + pnpm run build + rm -rf node_modules src + - name: Build module ZIP # IF YOU EDIT THIS, DON'T FORGET TO EDIT release.yml run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f178b1a93..cbaf91311 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,25 @@ jobs: with: php-version: '5.6' + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: views/js/admin/settings-app/pnpm-lock.yaml + + - name: Build React settings app + run: | + cd views/js/admin/settings-app + pnpm install --frozen-lockfile + pnpm run build + rm -rf node_modules src + - name: build # IF YOU EDIT THIS, DON'T FORGET TO EDIT deploy.yml run: | diff --git a/.gitignore b/.gitignore index c17ac67f2..39e1c9dfa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ /.idea /config*.xml /vendor -composer.lock .php_cs.cache tests/.env /node_modules var/cache/* !var/cache/index.php .DS_Store +views/js/admin/settings-app/node_modules/ +views/js/admin/dist/ diff --git a/Makefile b/Makefile index 5a9a74a41..427c46fcf 100644 --- a/Makefile +++ b/Makefile @@ -144,11 +144,29 @@ e2eh1786: test-e2e-headless-1786 test-e2e-headless-1786: make e2e1786p +# target: build-react - Install deps and build the React admin settings app +build-react: + cd views/js/admin/settings-app && pnpm install && pnpm run build + +# target: dev-react - Start React dev server with HMR +dev-react: + cd views/js/admin/settings-app && pnpm dev + +# target: watch-react - Build React app and watch for changes +watch-react: + cd views/js/admin/settings-app && pnpm run build --watch + +# target: lint-react - Run TypeScript type check +lint-react: + cd views/js/admin/settings-app && pnpm run tsc --noEmit + prepare-zip: rm -rf vendor && \ composer install --no-dev --optimize-autoloader && \ cp .github/.htaccess vendor/.htaccess && \ + cd views/js/admin/settings-app && pnpm install && pnpm run build && cd ../../../.. && \ rm -rf .git .github tests cypress .docker && \ + rm -rf views/js/admin/settings-app && \ mkdir saferpayofficial && \ rsync -Rr ./ ./saferpayofficial && \ find . -maxdepth 1 ! -name saferpayofficial -exec mv {} saferpayofficial/ \; && \ diff --git a/changelog.md b/changelog.md index b1d1a4fe8..ce9f6b82e 100644 --- a/changelog.md +++ b/changelog.md @@ -197,9 +197,24 @@ - Remove WL Crypto payment method - Added setting to toggle order confirmation email sending - Added feature to group card payment methods into unified "Card" payment method +- Fixed issue when newly enabled payment methods did not appear in checkout because default "all countries/currencies" restriction was not created on save +- Fixed issue when payment method country/currency dropdowns showed "0" instead of indicating that all countries/currencies are allowed +- BO : Added validation for Merchant Emails field (frontend + backend) to prevent saving invalid addresses ## [2.0.3] - Optimized database performance - Dynamic termimal selection - Removed uneccesary inputs from admin settings -- Checked overall module stability \ No newline at end of file +- Checked overall module stability + +## [2.1.0] +- BO : Redesigned back-office settings into a React single-page admin +- BO : Conditionally show/hide Saferpay Fields settings based on account license +- BO : Renamed the "Custom form" column in Payment methods to "Saferpay Fields" +- BO/FO : Accessibility improvements for EAA / WCAG 2.1 AA compliance +- Added configurable payment description and order reference on payment page +- API update to V1.50: added WERO and GIFTCARD payment methods, removed deprecated GIROPAY/PAYDIREKT/SOFORT +- BO : Fixed issue when a freshly installed module logged an account error before any API credentials were entered +- BO : Fixed issue when the "Could not reach your Saferpay account" warning kept showing after payment methods had loaded successfully +- Fixed issue when files removed in this version stayed on disk after an upgrade, leaving obsolete iframe checkout controllers reachable and re-creating obsolete menu tabs on module reset +- BO : Fixed issue when a saved API password offered no visible way to enter a new one, and browser password manager icons covered the show/hide password control diff --git a/composer.json b/composer.json index fef5eb7ff..51f919469 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,18 @@ }, "audit": { "ignore": ["PKSA-wws7-mr54-jsny"] + }, + "policy": { + "advisories": { + "ignore-id": [ + "PKSA-v5yj-8nmz-sk2q", + "PKSA-ft77-7h5f-p3r6", + "PKSA-b14r-zh1d-vdrc", + "PKSA-z3gr-8qht-p93v", + "PKSA-rkkf-636k-qjb3", + "PKSA-wws7-mr54-jsny" + ] + } } }, "repositories": { diff --git a/composer.lock b/composer.lock new file mode 100755 index 000000000..23a257fc4 --- /dev/null +++ b/composer.lock @@ -0,0 +1,4481 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "16c7f8f3b9124b664a93e77ae5003988", + "packages": [ + { + "name": "apimatic/unirest-php", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/apimatic/unirest-php.git", + "reference": "52e226fb3b7081dc9ef64aee876142a240a5f0f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/apimatic/unirest-php/zipball/52e226fb3b7081dc9ef64aee876142a240a5f0f9", + "reference": "52e226fb3b7081dc9ef64aee876142a240a5f0f9", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^5 || ^6 || ^7 || ^8 || ^9" + }, + "suggest": { + "ext-json": "Allows using JSON Bodies for sending and parsing requests" + }, + "type": "library", + "autoload": { + "psr-0": { + "Unirest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mashape", + "email": "opensource@mashape.com", + "homepage": "https://www.mashape.com", + "role": "Developer" + }, + { + "name": "APIMATIC", + "email": "opensource@apimatic.io", + "homepage": "https://www.apimatic.io", + "role": "Developer" + } + ], + "description": "Unirest PHP", + "homepage": "https://github.com/apimatic/unirest-php", + "keywords": [ + "client", + "curl", + "http", + "https", + "rest" + ], + "support": { + "email": "opensource@apimatic.io", + "issues": "https://github.com/apimatic/unirest-php/issues", + "source": "https://github.com/apimatic/unirest-php/tree/2.3.0" + }, + "time": "2022-06-15T08:29:49+00:00" + }, + { + "name": "invertus/knapsack", + "version": "10.0.2", + "source": { + "type": "git", + "url": "https://github.com/Invertus/Knapsack.git", + "reference": "5b36525742aafba2675c4a513b1459dc7d20627e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Invertus/Knapsack/zipball/5b36525742aafba2675c4a513b1459dc7d20627e", + "reference": "5b36525742aafba2675c4a513b1459dc7d20627e", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "require-dev": { + "ciaranmcnulty/phpspec-typehintedmethods": "^2.0", + "henrikbjorn/phpspec-code-coverage": "^3.0", + "phpmd/phpmd": "^2.0", + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7", + "squizlabs/php_codesniffer": "^3.4", + "symfony/console": "^2.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/collection_functions.php", + "src/utility_functions.php" + ], + "psr-4": { + "Invertus\\Knapsack\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Invertus\\Knapsack\\Tests\\Helpers\\": "tests/helpers/" + } + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dusan Kasan", + "email": "dusan@kasan.sk", + "homepage": "http://kasan.sk", + "role": "Developer" + } + ], + "description": "Collection library for PHP", + "homepage": "https://github.com/Invertus/Knapsack", + "keywords": [ + "collections", + "map", + "reduce", + "sequences" + ], + "support": { + "source": "https://github.com/Invertus/Knapsack/tree/10.0.2" + }, + "time": "2023-09-07T15:02:18+00:00" + }, + { + "name": "invertus/lock", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/Invertus/lock.git", + "reference": "07dedac8d18333b7bc19567e9056a95322161c05" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Invertus/lock/zipball/07dedac8d18333b7bc19567e9056a95322161c05", + "reference": "07dedac8d18333b7bc19567e9056a95322161c05", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/lock": "v3.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invertus\\Lock\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Invertus\\Lock\\Tests\\": "tests/" + } + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Invertus", + "email": "developers@invertus.eu", + "role": "Developer" + } + ], + "description": "The Lock Component creates and manages locks, a mechanism to provide exclusive access to a shared resource.", + "homepage": "https://github.com/Invertus/lock", + "time": "2024-04-11T07:30:43+00:00" + }, + { + "name": "league/container", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/container.git", + "reference": "84ecbc2dbecc31bd23faf759a0e329ee49abddbd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/container/zipball/84ecbc2dbecc31bd23faf759a0e329ee49abddbd", + "reference": "84ecbc2dbecc31bd23faf759a0e329ee49abddbd", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/container": "^1.0.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "replace": { + "orno/di": "~2.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0 || ^7.0", + "roave/security-advisories": "dev-latest", + "scrutinizer/ocular": "^1.8", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev", + "dev-3.x": "3.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Container\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Phil Bennett", + "email": "philipobenito@gmail.com", + "homepage": "http://www.philipobenito.com", + "role": "Developer" + } + ], + "description": "A fast and intuitive dependency injection container.", + "homepage": "https://github.com/thephpleague/container", + "keywords": [ + "container", + "dependency", + "di", + "injection", + "league", + "provider", + "service" + ], + "support": { + "issues": "https://github.com/thephpleague/container/issues", + "source": "https://github.com/thephpleague/container/tree/3.4.1" + }, + "funding": [ + { + "url": "https://github.com/philipobenito", + "type": "github" + } + ], + "time": "2021-07-09T08:23:52+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "eab7a0df01fe2344d172bff4cd6dbd3f8b84ad15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/eab7a0df01fe2344d172bff4cd6dbd3f8b84ad15", + "reference": "eab7a0df01fe2344d172bff4cd6dbd3f8b84ad15", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "phpunit/phpunit": "^6.5.14 || ^7.5.20 || ^8.5.19 || ^9.5.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.8.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2021-12-04T23:24:31+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/master" + }, + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "symfony/config", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "bc6b3fd3930d4b53a60b42fe2ed6fc466b75f03f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/bc6b3fd3930d4b53a60b42fe2ed6fc466b75f03f", + "reference": "bc6b3fd3930d4b53a60b42fe2ed6fc466b75f03f", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/filesystem": "~2.8|~3.0|~4.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/dependency-injection": "<3.3", + "symfony/finder": "<3.3" + }, + "require-dev": { + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/event-dispatcher": "~3.3|~4.0", + "symfony/finder": "~3.3|~4.0", + "symfony/yaml": "~3.0|~4.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "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": "Symfony Config Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v3.4.47" + }, + "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": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "51d2a2708c6ceadad84393f8581df1dcf9e5e84b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/51d2a2708c6ceadad84393f8581df1dcf9e5e84b", + "reference": "51d2a2708c6ceadad84393f8581df1dcf9e5e84b", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/container": "^1.0" + }, + "conflict": { + "symfony/config": "<3.3.7", + "symfony/finder": "<3.3", + "symfony/proxy-manager-bridge": "<3.4", + "symfony/yaml": "<3.4" + }, + "provide": { + "psr/container-implementation": "1.0" + }, + "require-dev": { + "symfony/config": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "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": "Symfony DependencyInjection Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v3.4.47" + }, + "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": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "e58d7841cddfed6e846829040dca2cca0ebbbbb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/e58d7841cddfed6e846829040dca2cca0ebbbbb3", + "reference": "e58d7841cddfed6e846829040dca2cca0ebbbbb3", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "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": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v3.4.47" + }, + "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": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/lock", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/lock.git", + "reference": "d7b8a52eed987bfa163aba175f3192dda07f120c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/lock/zipball/d7b8a52eed987bfa163aba175f3192dda07f120c", + "reference": "d7b8a52eed987bfa163aba175f3192dda07f120c", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/log": "~1.0", + "symfony/polyfill-php70": "~1.0" + }, + "require-dev": { + "predis/predis": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Lock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Jérémy Derussé", + "email": "jeremy@derusse.com" + } + ], + "description": "Symfony Lock Component", + "homepage": "https://symfony.com", + "keywords": [ + "cas", + "flock", + "locking", + "mutex", + "redlock", + "semaphore" + ], + "support": { + "source": "https://github.com/symfony/lock/tree/3.4" + }, + "time": "2017-11-22T12:18:49+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.30.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" + }, + "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-05-31T15:07:36+00:00" + }, + { + "name": "symfony/polyfill-php70", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/5f03a781d984aae42cebd18e7912fa80f02ee644", + "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "metapackage", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + }, + "branch-alias": { + "dev-main": "1.20-dev" + } + }, + "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": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php70/tree/v1.20.0" + }, + "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": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/yaml", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "88289caa3c166321883f67fe5130188ebbb47094" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/88289caa3c166321883f67fe5130188ebbb47094", + "reference": "88289caa3c166321883f67fe5130188ebbb47094", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/console": "<3.4" + }, + "require-dev": { + "symfony/console": "~3.4|~4.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "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": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v3.4.47" + }, + "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": "2020-10-24T10:57:07+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v3.6.10", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "5b547cdb25825f10251370f57ba5d9d924e6f68e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/5b547cdb25825f10251370f57ba5d9d924e6f68e", + "reference": "5b547cdb25825f10251370f57ba5d9d924e6f68e", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0", + "phpoption/phpoption": "^1.5.2", + "symfony/polyfill-ctype": "^1.17" + }, + "require-dev": { + "ext-filter": "*", + "ext-pcre": "*", + "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.21" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator.", + "ext-pcre": "Required to use most of the library." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v3.6.10" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2021-12-12T23:02:06+00:00" + } + ], + "packages-dev": [ + { + "name": "behat/behat", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Behat.git", + "reference": "08052f739619a9e9f62f457a67302f0715e6dd13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Behat/zipball/08052f739619a9e9f62f457a67302f0715e6dd13", + "reference": "08052f739619a9e9f62f457a67302f0715e6dd13", + "shasum": "" + }, + "require": { + "behat/gherkin": "^4.6.0", + "behat/transliterator": "^1.2", + "ext-mbstring": "*", + "php": ">=5.3.3", + "psr/container": "^1.0", + "symfony/config": "^2.7.51 || ^3.0 || ^4.0 || ^5.0", + "symfony/console": "^2.7.51 || ^2.8.33 || ^3.3.15 || ^3.4.3 || ^4.0.3 || ^5.0", + "symfony/dependency-injection": "^2.7.51 || ^3.0 || ^4.0 || ^5.0", + "symfony/event-dispatcher": "^2.7.51 || ^3.0 || ^4.0 || ^5.0", + "symfony/translation": "^2.7.51 || ^3.0 || ^4.0 || ^5.0", + "symfony/yaml": "^2.7.51 || ^3.0 || ^4.0 || ^5.0" + }, + "require-dev": { + "container-interop/container-interop": "^1.2", + "herrera-io/box": "~1.6.1", + "phpunit/phpunit": "^4.8.36 || ^6.5.14 || ^7.5.20", + "symfony/process": "~2.5 || ^3.0 || ^4.0 || ^5.0" + }, + "suggest": { + "ext-dom": "Needed to output test results in JUnit format." + }, + "bin": [ + "bin/behat" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Behat\\": "src/Behat/Behat/", + "Behat\\Testwork\\": "src/Behat/Testwork/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Scenario-oriented BDD framework for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "Agile", + "BDD", + "ScenarioBDD", + "Scrum", + "StoryBDD", + "User story", + "business", + "development", + "documentation", + "examples", + "symfony", + "testing" + ], + "support": { + "issues": "https://github.com/Behat/Behat/issues", + "source": "https://github.com/Behat/Behat/tree/v3.7.0" + }, + "time": "2020-06-03T13:08:44+00:00" + }, + { + "name": "behat/gherkin", + "version": "v4.7.3", + "source": { + "type": "git", + "url": "https://github.com/Behat/Gherkin.git", + "reference": "d5ae4616aeaa91daadbfb8446d9d17aae8d43cf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/d5ae4616aeaa91daadbfb8446d9d17aae8d43cf7", + "reference": "d5ae4616aeaa91daadbfb8446d9d17aae8d43cf7", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "cucumber/cucumber": "dev-gherkin-16.0.0", + "phpunit/phpunit": "^5.7.1|~6|~7", + "symfony/phpunit-bridge": "~2.7|~3|~4", + "symfony/yaml": "~2.3|~3|~4" + }, + "suggest": { + "symfony/yaml": "If you want to parse features, represented in YAML files" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Gherkin": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Gherkin DSL parser for PHP", + "homepage": "http://behat.org/", + "keywords": [ + "BDD", + "Behat", + "Cucumber", + "DSL", + "gherkin", + "parser" + ], + "support": { + "issues": "https://github.com/Behat/Gherkin/issues", + "source": "https://github.com/Behat/Gherkin/tree/v4.7.3" + }, + "time": "2021-02-04T12:26:47+00:00" + }, + { + "name": "behat/transliterator", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Transliterator.git", + "reference": "34490b42c5225687d9449e6476e66a03c62c43ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/34490b42c5225687d9449e6476e66a03c62c43ff", + "reference": "34490b42c5225687d9449e6476e66a03c62c43ff", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "chuyskywalker/rolling-curl": "^3.1", + "php-yaoi/php-yaoi": "^1.0", + "phpunit/phpunit": "^4.8.36 || ^6.5.14 || ^8.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Transliterator\\": "src/Behat/Transliterator" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Artistic-1.0" + ], + "description": "String transliterator", + "keywords": [ + "i18n", + "slug", + "transliterator" + ], + "support": { + "issues": "https://github.com/Behat/Transliterator/issues", + "source": "https://github.com/Behat/Transliterator/tree/v1.4.0" + }, + "abandoned": true, + "time": "2022-03-30T09:16:18+00:00" + }, + { + "name": "composer/pcre", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/67a32d7d6f9f560b726ab25a061b38ff3a80c560", + "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/1.0.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-01-21T20:24:37+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "9e36aeed4616366d2b690bdce11f71e9178c579a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/9e36aeed4616366d2b690bdce11f71e9178c579a", + "reference": "9e36aeed4616366d2b690bdce11f71e9178c579a", + "shasum": "" + }, + "require": { + "composer/pcre": "^1", + "php": "^5.3.2 || ^7.0 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/2.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-24T20:20:32+00:00" + }, + { + "name": "doctrine/annotations", + "version": "1.14.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "253dca476f70808a5aeed3a47cc2cc88c5cab915" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/253dca476f70808a5aeed3a47cc2cc88c5cab915", + "reference": "253dca476f70808a5aeed3a47cc2cc88c5cab915", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1 || ^2", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "~1.4.10 || ^1.10.28", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.14.4" + }, + "abandoned": true, + "time": "2024-09-05T10:15:52+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, + { + "name": "doctrine/lexer", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6", + "reference": "861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^4.11 || ^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:35:39+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v2.19.3", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", + "reference": "75ac86f33fab4714ea5a39a396784d83ae3b5ed8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/75ac86f33fab4714ea5a39a396784d83ae3b5ed8", + "reference": "75ac86f33fab4714ea5a39a396784d83ae3b5ed8", + "shasum": "" + }, + "require": { + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^1.2 || ^2.0", + "doctrine/annotations": "^1.2", + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^5.6 || ^7.0 || ^8.0", + "php-cs-fixer/diff": "^1.3", + "symfony/console": "^3.4.43 || ^4.1.6 || ^5.0", + "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0", + "symfony/filesystem": "^3.0 || ^4.0 || ^5.0", + "symfony/finder": "^3.0 || ^4.0 || ^5.0", + "symfony/options-resolver": "^3.0 || ^4.0 || ^5.0", + "symfony/polyfill-php70": "^1.0", + "symfony/polyfill-php72": "^1.4", + "symfony/process": "^3.0 || ^4.0 || ^5.0", + "symfony/stopwatch": "^3.0 || ^4.0 || ^5.0" + }, + "require-dev": { + "justinrainbow/json-schema": "^5.0", + "keradus/cli-executor": "^1.4", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.4.2", + "php-cs-fixer/accessible-object": "^1.0", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", + "phpspec/prophecy-phpunit": "^1.1 || ^2.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.13 || ^9.5", + "phpunitgoodpractices/polyfill": "^1.5", + "phpunitgoodpractices/traits": "^1.9.1", + "sanmai/phpunit-legacy-adapter": "^6.4 || ^8.2.1", + "symfony/phpunit-bridge": "^5.2.1", + "symfony/yaml": "^3.0 || ^4.0 || ^5.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters.", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "For IsIdenticalString constraint.", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "For XmlMatchesXsd constraint.", + "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "extra": { + "branch-alias": { + "dev-master": "2.19-dev" + } + }, + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "classmap": [ + "tests/Test/AbstractFixerTestCase.php", + "tests/Test/AbstractIntegrationCaseFactory.php", + "tests/Test/AbstractIntegrationTestCase.php", + "tests/Test/Assert/AssertTokensTrait.php", + "tests/Test/IntegrationCase.php", + "tests/Test/IntegrationCaseFactory.php", + "tests/Test/IntegrationCaseFactoryInterface.php", + "tests/Test/InternalIntegrationCaseFactory.php", + "tests/Test/IsIdenticalConstraint.php", + "tests/Test/TokensWithObservedTransformers.php", + "tests/TestCase.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "support": { + "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.19.3" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2021-11-15T17:17:55+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/bb87e28e7d7b8d9a7fda231d37457c9210faf6ce", + "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v3.1.5" + }, + "time": "2018-02-28T20:30:58+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, + "time": "2018-07-08T19:23:20+00:00" + }, + { + "name": "phar-io/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/master" + }, + "time": "2018-07-08T19:19:57+00:00" + }, + { + "name": "php-cs-fixer/diff", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/diff.git", + "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/dbd31aeb251639ac0b9e7e29405c1441907f5759", + "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", + "symfony/process": "^3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "SpacePossum" + } + ], + "description": "sebastian/diff v2 backport support for PHP5.6", + "homepage": "https://github.com/PHP-CS-Fixer", + "keywords": [ + "diff" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/diff/issues", + "source": "https://github.com/PHP-CS-Fixer/diff/tree/v1.3.1" + }, + "abandoned": true, + "time": "2020-10-14T08:39:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b", + "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/master" + }, + "time": "2020-04-27T09:25:28+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "4.3.4", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c", + "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", + "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "doctrine/instantiator": "^1.0.5", + "mockery/mockery": "^1.0", + "phpdocumentor/type-resolver": "0.4.*", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/4.x" + }, + "time": "2019-12-28T18:55:12+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", + "shasum": "" + }, + "require": { + "php": "^7.1", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "^7.1", + "mockery/mockery": "~1", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/0.7.2" + }, + "time": "2019-08-22T18:11:29+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.10.3", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "451c3cd1418cf640de218914901e51b064abb093" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", + "reference": "451c3cd1418cf640de218914901e51b064abb093", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", + "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5 || ^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.10.3" + }, + "time": "2020-03-05T15:02:03+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "6.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.1", + "phpunit/php-file-iterator": "^2.0", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.1 || ^4.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "suggest": { + "ext-xdebug": "^2.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/master" + }, + "time": "2018-10-31T16:06:48+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "2.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "69deeb8664f611f156a924154985fbd4911eb36b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/69deeb8664f611f156a924154985fbd4911eb36b", + "reference": "69deeb8664f611f156a924154985fbd4911eb36b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:39:50+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + }, + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "2.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "a691211e94ff39a34811abd521c31bd5b305b0bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a691211e94ff39a34811abd521c31bd5b305b0bb", + "reference": "a691211e94ff39a34811abd521c31bd5b305b0bb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:42:41+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "3.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "9c1da83261628cb24b6a6df371b6e312b3954768" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9c1da83261628cb24b6a6df371b6e312b3954768", + "reference": "9c1da83261628cb24b6a6df371b6e312b3954768", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-07-26T12:15:06+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "7.5.20", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.7", + "phar-io/manifest": "^1.0.2", + "phar-io/version": "^2.0", + "php": "^7.1", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^6.0.7", + "phpunit/php-file-iterator": "^2.0.1", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0", + "sebastian/environment": "^4.0", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpunit/phpunit-mock-objects": "*" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*", + "phpunit/php-invoker": "^2.0" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/7.5.20" + }, + "time": "2020-01-08T08:45:45+00:00" + }, + { + "name": "prestashop/autoindex", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/PrestaShopCorp/autoindex.git", + "reference": "92e10242f94a99163dece280f6bd7b7c2b79c158" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PrestaShopCorp/autoindex/zipball/92e10242f94a99163dece280f6bd7b7c2b79c158", + "reference": "92e10242f94a99163dece280f6bd7b7c2b79c158", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^3.1", + "php": ">=5.6", + "symfony/console": "^3.4", + "symfony/finder": "^3.4" + }, + "bin": [ + "bin/autoindex" + ], + "type": "library", + "autoload": { + "psr-4": { + "PrestaShop\\AutoIndex\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "AFL-3.0" + ], + "authors": [ + { + "name": "PrestaShop SA", + "email": "contact@prestashop.com" + } + ], + "description": "Automatically add an 'index.php' in all the current or specified directories and all sub-directories.", + "homepage": "https://github.com/PrestaShopCorp/autoindex", + "support": { + "source": "https://github.com/PrestaShopCorp/autoindex/tree/v1.0.0" + }, + "time": "2020-03-11T13:37:03+00:00" + }, + { + "name": "prestashop/header-stamp", + "version": "v1.7", + "source": { + "type": "git", + "url": "https://github.com/PrestaShopCorp/header-stamp.git", + "reference": "d77ce6d0a7f066670a4774be88f05e5f07b4b6fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PrestaShopCorp/header-stamp/zipball/d77ce6d0a7f066670a4774be88f05e5f07b4b6fc", + "reference": "d77ce6d0a7f066670a4774be88f05e5f07b4b6fc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^3.1", + "php": ">=5.6", + "symfony/console": "^3.4 || ~4.0 || ~5.0", + "symfony/finder": "^3.4 || ~4.0 || ~5.0" + }, + "require-dev": { + "prestashop/php-dev-tools": "1.*" + }, + "bin": [ + "bin/header-stamp" + ], + "type": "library", + "autoload": { + "psr-4": { + "PrestaShop\\HeaderStamp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "AFL-3.0" + ], + "authors": [ + { + "name": "PrestaShop SA", + "email": "contact@prestashop.com" + } + ], + "description": "Rewrite your file headers to add the license or to make them up-to-date", + "homepage": "https://github.com/PrestaShopCorp/header-stamp", + "support": { + "issues": "https://github.com/PrestaShopCorp/header-stamp/issues", + "source": "https://github.com/PrestaShopCorp/header-stamp/tree/v1.7" + }, + "time": "2020-12-09T16:40:38+00:00" + }, + { + "name": "prestashop/php-dev-tools", + "version": "v3.16.1", + "source": { + "type": "git", + "url": "https://github.com/PrestaShop/php-dev-tools.git", + "reference": "785108c29ef6f580930372d88b8f551740fdee98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PrestaShop/php-dev-tools/zipball/785108c29ef6f580930372d88b8f551740fdee98", + "reference": "785108c29ef6f580930372d88b8f551740fdee98", + "shasum": "" + }, + "require": { + "friendsofphp/php-cs-fixer": "^2.14", + "php": ">=5.6.0", + "prestashop/autoindex": "^1.0", + "prestashop/header-stamp": "^1.0", + "squizlabs/php_codesniffer": "^3.4", + "symfony/console": "~3.2 || ~4.0 || ~5.0", + "symfony/filesystem": "~3.2 || ~4.0 || ~5.0" + }, + "conflict": { + "friendsofphp/php-cs-fixer": "2.18.3" + }, + "bin": [ + "bin/prestashop-coding-standards" + ], + "type": "library", + "autoload": { + "psr-4": { + "PrestaShop\\CodingStandards\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PrestaShop coding standards", + "support": { + "issues": "https://github.com/PrestaShop/php-dev-tools/issues", + "source": "https://github.com/PrestaShop/php-dev-tools/tree/v3.16.1" + }, + "time": "2021-10-18T07:48:21+00:00" + }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, + "time": "2016-08-06T20:24:11+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54", + "reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:45:45+00:00" + }, + { + "name": "sebastian/comparator", + "version": "3.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "bc7d8ac2fe1cce229bff9b5fd4efe65918a1ff52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/bc7d8ac2fe1cce229bff9b5fd4efe65918a1ff52", + "reference": "bc7d8ac2fe1cce229bff9b5fd4efe65918a1ff52", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:20:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "98ff311ca519c3aa73ccd3de053bdb377171d7b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/98ff311ca519c3aa73ccd3de053bdb377171d7b6", + "reference": "98ff311ca519c3aa73ccd3de053bdb377171d7b6", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:16:36+00:00" + }, + { + "name": "sebastian/environment", + "version": "4.2.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "56932f6049a0482853056ffd617c91ffcc754205" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/56932f6049a0482853056ffd617c91ffcc754205", + "reference": "56932f6049a0482853056ffd617c91ffcc754205", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/4.2.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:49:59+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/73a9676f2833b9a7c36968f9d882589cd75511e6", + "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:00:17+00:00" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0" + }, + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "ac5b293dba925751b808e02923399fb44ff0d541" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/ac5b293dba925751b808e02923399fb44ff0d541", + "reference": "ac5b293dba925751b808e02923399fb44ff0d541", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:54:02+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "1d439c229e61f244ff1f211e5c99737f90c67def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/1d439c229e61f244ff1f211e5c99737f90c67def", + "reference": "1d439c229e61f244ff1f211e5c99737f90c67def", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:56:04+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "8fe7e75986a9d24b4cceae847314035df7703a5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/8fe7e75986a9d24b4cceae847314035df7703a5a", + "reference": "8fe7e75986a9d24b4cceae847314035df7703a5a", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T05:25:53+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "72a7f7674d053d548003b16ff5a106e7e0e06eee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/72a7f7674d053d548003b16ff5a106e7e0e06eee", + "reference": "72a7f7674d053d548003b16ff5a106e7e0e06eee", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-01T13:59:09+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/master" + }, + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-04T16:30:35+00:00" + }, + { + "name": "symfony/console", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "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": "Symfony Console Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/v3.4.47" + }, + "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": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/debug", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "ab42889de57fdfcfcc0759ab102e2fd4ea72dcae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/ab42889de57fdfcfcc0759ab102e2fd4ea72dcae", + "reference": "ab42889de57fdfcfcc0759ab102e2fd4ea72dcae", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/http-kernel": "~2.8|~3.0|~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "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": "Symfony Debug Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/debug/tree/v3.4.47" + }, + "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" + } + ], + "abandoned": "symfony/error-handler", + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "31fde73757b6bad247c54597beef974919ec6860" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/31fde73757b6bad247c54597beef974919ec6860", + "reference": "31fde73757b6bad247c54597beef974919ec6860", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "conflict": { + "symfony/dependency-injection": "<3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0|~4.0", + "symfony/debug": "~3.4|~4.4", + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/stopwatch": "~2.8|~3.0|~4.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "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": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v3.4.47" + }, + "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": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/finder", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "b6b6ad3db3edb1b4b1c1896b1975fb684994de6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/b6b6ad3db3edb1b4b1c1896b1975fb684994de6e", + "reference": "b6b6ad3db3edb1b4b1c1896b1975fb684994de6e", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "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": "Symfony Finder Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v3.4.47" + }, + "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": "2020-11-16T17:02:08+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "c7efc97a47b2ebaabc19d5b6c6b50f5c37c92744" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/c7efc97a47b2ebaabc19d5b6c6b50f5c37c92744", + "reference": "c7efc97a47b2ebaabc19d5b6c6b50f5c37c92744", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "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": "Symfony OptionsResolver Component", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v3.4.47" + }, + "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": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.30.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "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": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + }, + "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-06-19T12:30:46+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.30.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "10112722600777e02d2745716b70c5db4ca70442" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/10112722600777e02d2745716b70c5db4ca70442", + "reference": "10112722600777e02d2745716b70c5db4ca70442", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "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": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.30.0" + }, + "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-06-19T12:30:46+00:00" + }, + { + "name": "symfony/process", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "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": "Symfony Process Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v3.4.47" + }, + "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": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "298b81faad4ce60e94466226b2abbb8c9bca7462" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/298b81faad4ce60e94466226b2abbb8c9bca7462", + "reference": "298b81faad4ce60e94466226b2abbb8c9bca7462", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "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": "Symfony Stopwatch Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v3.4.47" + }, + "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": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/translation", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "be83ee6c065cb32becdb306ba61160d598b1ce88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/be83ee6c065cb32becdb306ba61160d598b1ce88", + "reference": "be83ee6c065cb32becdb306ba61160d598b1ce88", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.4", + "symfony/yaml": "<3.4" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/finder": "~2.8|~3.0|~4.0", + "symfony/http-kernel": "~3.4|~4.0", + "symfony/intl": "^2.8.18|^3.2.5|~4.0", + "symfony/var-dumper": "~3.4|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "psr/log-implementation": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "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": "Symfony Translation Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v3.4.47" + }, + "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": "2020-10-24T10:57:07+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/master" + }, + "time": "2019-06-13T22:48:21+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.9.1" + }, + "time": "2020-07-08T17:02:28+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.1" + }, + "platform-dev": {}, + "platform-overrides": { + "php": "7.1" + }, + "plugin-api-version": "2.9.0" +} diff --git a/controllers/admin/AdminSaferPayOfficialFieldsController.php b/controllers/admin/AdminSaferPayOfficialFieldsController.php deleted file mode 100644 index 7508d5986..000000000 --- a/controllers/admin/AdminSaferPayOfficialFieldsController.php +++ /dev/null @@ -1,90 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ - -use Invertus\SaferPay\Config\SaferPayConfig; - -require_once dirname(__FILE__) . '/../../vendor/autoload.php'; - -if (!defined('_PS_VERSION_')) { - exit; -} - -class AdminSaferPayOfficialFieldsController extends ModuleAdminController -{ - public function __construct() - { - parent::__construct(); - $this->bootstrap = true; - - $this->tpl_folder = 'field-option-settings/'; - $this->initOptions(); - } - - public function initContent() - { - parent::initContent(); - } - - public function initOptions() - { - $this->fields_options = [ - 'hosted_fields_settings' => [ - 'title' => $this->module->l('Hosted fields settings'), - 'icon' => 'icon-settings', - 'fields' => [ - SaferPayConfig::HOSTED_FIELDS_TEMPLATE . '_description' => [ - 'type' => 'desc', - 'class' => 'col-lg-12', - 'template' => 'field-hosted-field-template-desc.tpl', - ], - - SaferPayConfig::HOSTED_FIELDS_TEMPLATE => [ - 'type' => 'select-template', - 'name' => SaferPayConfig::HOSTED_FIELDS_TEMPLATE, - 'templateOptions' => [ - "{$this->module->getPathUri()}views/img/hosted-templates/template1.jpg", - "{$this->module->getPathUri()}views/img/hosted-templates/template2.jpg", - "{$this->module->getPathUri()}views/img/hosted-templates/template3.jpg", - ], - ], - ], - 'buttons' => [ - 'save_and_connect' => [ - 'title' => $this->module->l('Save'), - 'icon' => 'process-icon-save', - 'class' => 'btn btn-default pull-right', - 'type' => 'submit', - ], - ], - ], - ]; - } - - public function setMedia($isNewTheme = false) - { - parent::setMedia($isNewTheme); - - $this->addJS('modules/' . $this->module->name . '/views/js/admin/saferpay_fields.js'); - $this->addCSS('modules/' . $this->module->name . '/views/css/admin/saferpay_fields.css'); - } -} diff --git a/controllers/admin/AdminSaferPayOfficialPaymentController.php b/controllers/admin/AdminSaferPayOfficialPaymentController.php old mode 100644 new mode 100755 index 6d2102d2a..105ce2eb7 --- a/controllers/admin/AdminSaferPayOfficialPaymentController.php +++ b/controllers/admin/AdminSaferPayOfficialPaymentController.php @@ -27,313 +27,14 @@ exit; } -use Invertus\SaferPay\Config\SaferPayConfig; -use Invertus\SaferPay\Exception\Restriction\RestrictionException; -use Invertus\SaferPay\Repository\SaferPayFieldRepository; -use Invertus\SaferPay\Repository\SaferPayLogoRepository; -use Invertus\SaferPay\Repository\SaferPayPaymentRepository; -use Invertus\SaferPay\Repository\SaferPayRestrictionRepository; -use Invertus\SaferPay\Service\SaferPayExceptionService; -use Invertus\SaferPay\Service\SaferPayFieldCreator; -use Invertus\SaferPay\Service\SaferPayLogoCreator; -use Invertus\SaferPay\Service\SaferPayPaymentCreator; -use Invertus\SaferPay\Service\SaferPayPaymentNotation; -use Invertus\SaferPay\Service\SaferPayRestrictionCreator; -use Invertus\SaferPay\Service\SaferPayObtainPaymentMethods; -use Invertus\SaferPay\Service\SaferPayRefreshPaymentsService; -use Invertus\SaferPay\Exception\Api\SaferPayApiException; - class AdminSaferPayOfficialPaymentController extends ModuleAdminController { /** @var \SaferPayOfficial */ public $module; - public function __construct() - { - parent::__construct(); - $this->bootstrap = true; - } - - public function setMedia($isNewTheme = false) - { - parent::setMedia($isNewTheme); - - $this->addCSS('modules/' . $this->module->name . '/views/css/admin/payment_method.css'); - $this->addJS('modules/' . $this->module->name . '/views/js/admin/chosen_countries.js'); - $this->addJS('modules/' . $this->module->name . '/views/js/admin/payment_method_all.js'); - } - - /** - * Custom form processing - */ - public function postProcess() - { - // Refresh payments. - /** @var SaferPayRefreshPaymentsService $refreshPaymentsService */ - $refreshPaymentsService = $this->module->getService(SaferPayRefreshPaymentsService::class); - try { - $refreshPaymentsService->refreshPayments(); - } catch (SaferPayApiException $exception) { - $this->errors[] = $this->module->l($exception->getMessage()); - } - - if (!Tools::isSubmit('submitAddconfiguration')) { - return parent::postProcess(); - } - - /** @var SaferPayPaymentCreator $paymentCreation */ - $paymentCreation = $this->module->getService(SaferPayPaymentCreator::class); - - /** @var SaferPayLogoCreator $logoCreation */ - $logoCreation = $this->module->getService(SaferPayLogoCreator::class); - - /** @var SaferPayFieldCreator $fieldCreation */ - $fieldCreation = $this->module->getService(SaferPayFieldCreator::class); - - /** @var SaferPayRestrictionCreator $restrictionCreator */ - $restrictionCreator = $this->module->getService(SaferPayRestrictionCreator::class); - - $paymentMethods = $this->getPaymentMethods(); - if (is_null($paymentMethods)) { - return; - } - - $success = true; - foreach ($paymentMethods as $paymentMethod) { - $isActive = Tools::getValue($paymentMethod . '_enable'); - $success &= $paymentCreation->updatePayment($paymentMethod, $isActive); - - $isActive = Tools::getValue($paymentMethod . '_logo'); - $success &= $logoCreation->updateLogo($paymentMethod, $isActive); - - $isActive = Tools::getValue($paymentMethod . '_field'); - $success &= $fieldCreation->updateField($paymentMethod, $isActive); - - try { - $success &= $restrictionCreator->updateRestriction( - $paymentMethod, - SaferPayRestrictionCreator::RESTRICTION_COUNTRY, - Tools::getValue($paymentMethod . SaferPayRestrictionCreator::COUNTRY_SUFFIX) - ); - $success &= $restrictionCreator->updateRestriction( - $paymentMethod, - SaferPayRestrictionCreator::RESTRICTION_CURRENCY, - Tools::getValue($paymentMethod . SaferPayRestrictionCreator::CURRENCY_SUFFIX) - ); - } catch (RestrictionException $e) { - $this->errors[] = $this->module->l('Wrong restriction type'); - $success = false; - } - } - - if (!$success) { - $this->errors[] = $this->module->l('Failed update'); - } else { - $this->confirmations[] = $this->module->l('Successful update'); - } - - return true; - } - - public function initContent() - { - parent::initContent(); - - $this->content .= $this->renderShoppingPointOptions(); - $this->context->smarty->assign('content', $this->content); - } - - protected function renderShoppingPointOptions() + public function init() { - $referralOptionsForm = new HelperForm(); - - /** @var SaferPayPaymentRepository $paymentRepository */ - $paymentRepository = $this->module->getService(SaferPayPaymentRepository::class); - - /** @var SaferPayLogoRepository $logoRepository */ - $logoRepository = $this->module->getService(SaferPayLogoRepository::class); - - /** @var SaferPayFieldRepository $fieldRepository */ - $fieldRepository = $this->module->getService(SaferPayFieldRepository::class); - - /** @var SaferPayRestrictionRepository $restrictionRepository */ - $restrictionRepository = $this->module->getService(SaferPayRestrictionRepository::class); - - $paymentMethods = $this->getPaymentMethods(); - if (is_null($paymentMethods) || empty($paymentMethods)) { - $this->errors[] = $this->module->l('No payment methods available. Please check your SaferPay account configuration.'); - - return ''; - } - - $this->initForm(); - $fieldsForm = []; - $fieldsForm[0]['form'] = $this->fields_form; - - /** @var SaferPayObtainPaymentMethods $saferPayObtainPaymentMethods */ - $saferPayObtainPaymentMethods = $this->module->getService(SaferPayObtainPaymentMethods::class); - - $paymentMethodsList = $saferPayObtainPaymentMethods->obtainPaymentMethods(); - - foreach ($paymentMethods as $paymentMethod) { - $isActive = $paymentRepository->isActiveByName($paymentMethod); - $isLogoActive = $logoRepository->isActiveByName($paymentMethod); - $isFieldActive = $fieldRepository->isActiveByName($paymentMethod); - $selectedCountries = $restrictionRepository->getSelectedIdsByName( - $paymentMethod, - SaferPayRestrictionCreator::RESTRICTION_COUNTRY - ); - $selectedCurrencies = $restrictionRepository->getSelectedIdsByName( - $paymentMethod, - SaferPayRestrictionCreator::RESTRICTION_CURRENCY - ); - - $this->context->smarty->assign( - [ - 'is_active' => $isActive, - 'is_logo_active' => $isLogoActive, - 'paymentMethod' => $paymentMethod, - 'countryOptions' => $this->getActiveCountriesList(), - 'countrySelect' => $selectedCountries, - 'currencyOptions' => $this->getActiveCurrenciesList($paymentMethod, $paymentMethodsList), - 'currencySelect' => $selectedCurrencies, - 'is_field_active' => $isFieldActive, - 'supported_field_payments' => SaferPayConfig::FIELD_SUPPORTED_PAYMENT_METHODS, - ] - ); - $referralOptionsForm->fields_value[$paymentMethod] = - $this->context->smarty->fetch( - $this->module->getLocalPath() . 'views/templates/admin/payment_method.tpl' - ); - } - $this->context->smarty->assign([ - 'countryOptions' => $this->getActiveCountriesList(), - 'countrySelect' => [], - 'currencyOptions' => [0 => $this->module->l('All')], - 'currencySelect' => [], - ]); - $referralOptionsForm->fields_value['all'] = - $this->context->smarty->fetch( - $this->module->getLocalPath() . 'views/templates/admin/payment_method_all.tpl' - ); - $referralOptionsForm->fields_value['payment_method_label'] = - $this->context->smarty->fetch( - $this->module->getLocalPath() . 'views/templates/admin/payment_method_label.tpl' - ); - $this->content .= $referralOptionsForm->generateForm($fieldsForm); - } - - public function getActiveCountriesList($onlyActive = true) - { - $langId = $this->context->language->id; - $countries = Country::getCountries($langId, $onlyActive); - $countriesWithNames = []; - $countriesWithNames[0] = $this->module->l('All'); - foreach ($countries as $key => $country) { - $countriesWithNames[$key] = $country['name']; - } - - return $countriesWithNames; - } - - public function getActiveCurrenciesList($paymentMethod, $paymentMethods) - { - $currencyOptions[0] = $this->module->l('All'); - - if (!isset($paymentMethods[$paymentMethod]['currencies']) && in_array($paymentMethod, SaferPayConfig::WALLET_PAYMENT_METHODS)) { - foreach (Currency::getCurrencies() as $currency) { - $currencyOptions[$currency['id_currency']] = $currency['iso_code']; - } - - return $currencyOptions; - } - - foreach ($paymentMethods[$paymentMethod]['currencies'] as $currencyIso) { - if (Currency::getIdByIsoCode($currencyIso)) { - $currencyOptions[Currency::getIdByIsoCode($currencyIso)] = $currencyIso; - } - } - - return $currencyOptions; - } - - protected function initForm() - { - $fields = []; - $fields[] = [ - 'type' => 'free', - 'label' => '', - 'name' => 'payment_method_label', - ]; - $fields[] = [ - 'type' => 'free', - 'label' => $this->module->l('All payments'), - 'name' => 'all', - 'form_group_class' => 'saferpay-group all-payments', - ]; - - try { - /** @var SaferPayObtainPaymentMethods $saferPayObtainPaymentMethods */ - $saferPayObtainPaymentMethods = $this->module->getService(SaferPayObtainPaymentMethods::class); - $paymentMethods = $saferPayObtainPaymentMethods->obtainPaymentMethodsNamesAsArray(); - } catch (SaferPayApiException $exception) { - /** @var SaferPayExceptionService $exceptionService */ - $exceptionService = $this->module->getService(SaferPayExceptionService::class); - $saferPayErrors = json_decode($this->context->cookie->saferPayErrors, true); - $saferPayErrors[] = $exceptionService->getErrorMessageForException( - $exception, - $exceptionService->getErrorMessages() - ); - $this->context->cookie->saferPayErrors = json_encode($saferPayErrors); - - $this->errors[] = $this->module->l('Please connect to SaferPay system to allowed payment methods.'); - - return; - } - /** @var SaferPayPaymentNotation $saferPayPaymentNotation */ - $saferPayPaymentNotation = $this->module->getService(SaferPayPaymentNotation::class); - - foreach ($paymentMethods as $paymentMethod) { - $fields[] = [ - 'type' => 'free', - 'label' => $saferPayPaymentNotation->getForDisplay($paymentMethod), - 'name' => $paymentMethod, - 'form_group_class' => 'saferpay-group', - ]; - } - - $this->fields_form = [ - 'legend' => [ - 'title' => $this->module->l('Payments'), - ], - 'input' => - $fields, - 'submit' => [ - 'title' => $this->module->l('Save'), - ], - ]; - } - - private function getPaymentMethods() - { - try { - /** @var SaferPayObtainPaymentMethods $saferPayObtainPaymentMethods */ - $saferPayObtainPaymentMethods = $this->module->getService(SaferPayObtainPaymentMethods::class); - - return $saferPayObtainPaymentMethods->obtainPaymentMethodsNamesAsArray(); - } catch (SaferPayApiException $exception) { - /** @var SaferPayExceptionService $exceptionService */ - $exceptionService = $this->module->getService(SaferPayExceptionService::class); - $saferPayErrors = json_decode($this->context->cookie->saferPayErrors, true); - $saferPayErrors[] = $exceptionService->getErrorMessageForException( - $exception, - $exceptionService->getErrorMessages() - ); - $this->context->cookie->saferPayErrors = json_encode($saferPayErrors); - - $this->errors[] = $this->module->l('To see available payment methods, you must connect to your SaferPay account.'); - - return null; - } + parent::init(); + Tools::redirectAdmin($this->context->link->getAdminLink('AdminSaferPayOfficialSettings')); } } diff --git a/controllers/admin/AdminSaferPayOfficialSettingsController.php b/controllers/admin/AdminSaferPayOfficialSettingsController.php old mode 100644 new mode 100755 index 701ffa418..cfb388b99 --- a/controllers/admin/AdminSaferPayOfficialSettingsController.php +++ b/controllers/admin/AdminSaferPayOfficialSettingsController.php @@ -22,9 +22,24 @@ */ use Invertus\SaferPay\Config\SaferPayConfig; +use Invertus\SaferPay\Repository\SaferPayFieldRepository; +use Invertus\SaferPay\Repository\SaferPayLogoRepository; +use Invertus\SaferPay\Repository\SaferPayPaymentRepository; +use Invertus\SaferPay\Repository\SaferPayRestrictionRepository; use Invertus\SaferPay\Repository\SaferPaySavedCreditCardRepository; -use Invertus\SaferPay\Adapter\Configuration; -use Invertus\SaferPay\Service\SaferPayTerminalService; +use Invertus\SaferPay\Adapter\Configuration as SaferPayConfiguration; +use Invertus\SaferPay\Service\SaferPayFieldCreator; +use Invertus\SaferPay\Service\SaferPayGenerateFieldAccessToken; +use Invertus\SaferPay\Service\SaferPayGetLicense; +use Invertus\SaferPay\Service\SaferPayGetTerminals; +use Invertus\SaferPay\Service\SaferPayLogoCreator; +use Invertus\SaferPay\Service\SaferPayObtainPaymentMethods; +use Invertus\SaferPay\Service\SaferPayPaymentCreator; +use Invertus\SaferPay\Service\SaferPayPaymentNotation; +use Invertus\SaferPay\Service\SaferPayRefreshPaymentsService; +use Invertus\SaferPay\Service\SaferPayRestrictionCreator; +use Invertus\SaferPay\Exception\Api\SaferPayApiException; +use Invertus\SaferPay\Exception\Restriction\RestrictionException; use Invertus\SaferPay\Logger\LoggerInterface; require_once dirname(__FILE__) . '/../../vendor/autoload.php'; @@ -36,600 +51,864 @@ class AdminSaferPayOfficialSettingsController extends ModuleAdminController { const FILE_NAME = 'AdminSaferPayOfficialSettingsController'; + const PASSWORD_PLACEHOLDER = '********'; + + const ALLOWED_AJAX_ACTIONS = [ + 'saveCredentials', + 'savePaymentProcessing', + 'saveEmailSettings', + 'saveGeneralSettings', + 'savePaymentMethods', + 'getTerminals', + 'generateFieldAccessToken', + 'refreshData', + ]; + + /** + * AJAX actions that change state and therefore require 'edit' permission. + */ + const STATE_CHANGING_AJAX_ACTIONS = [ + 'saveCredentials', + 'savePaymentProcessing', + 'saveEmailSettings', + 'saveGeneralSettings', + 'savePaymentMethods', + 'generateFieldAccessToken', + ]; /** @var \SaferPayOfficial */ public $module; + /** + * Set when the Saferpay account could not be reached while building the + * payment methods list, so the response can surface it to the merchant. + * + * @var bool + */ + private $paymentMethodsFetchFailed = false; + public function __construct() { parent::__construct(); $this->bootstrap = true; + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); - $this->tpl_folder = 'field-option-settings/'; - $this->initOptions(); + $distPath = 'modules/' . $this->module->name . '/views/js/admin/dist/'; + $this->addJS($distPath . 'saferpay-settings.js'); + $this->addCSS($distPath . 'saferpay-settings.css'); } public function initContent() { parent::initContent(); + + $settingsData = $this->collectSettingsData(); + + $this->context->smarty->assign([ + 'settingsDataJson' => json_encode($settingsData), + ]); + + $this->content .= $this->context->smarty->fetch( + $this->module->getLocalPath() . 'views/templates/admin/settings_react.tpl' + ); + $this->context->smarty->assign('content', $this->content); } public function postProcess() { - parent::postProcess(); + if (!$this->isAjax()) { + return parent::postProcess(); + } - /** @var Configuration $configuration */ - $configuration = $this->module->getService(Configuration::class); + if (!$this->validateAjaxToken()) { + $this->ajaxResponse(false, $this->module->l('Invalid security token', self::FILE_NAME)); - $isCreditCardSaveEnabled = $configuration->get(SaferPayConfig::CREDIT_CARD_SAVE); + return false; + } - if (!$isCreditCardSaveEnabled) { - /** @var SaferPaySavedCreditCardRepository $cardRepo */ - $cardRepo = $this->module->getService(SaferPaySavedCreditCardRepository::class); - $cardRepo->deleteAllSavedCreditCards(); + $action = Tools::getValue('action'); + if (!$action || !in_array($action, self::ALLOWED_AJAX_ACTIONS)) { + $this->ajaxResponse(false, $this->module->l('Invalid action', self::FILE_NAME)); + + return false; } - $haveFieldToken = $configuration->get(SaferPayConfig::FIELDS_ACCESS_TOKEN . SaferPayConfig::getConfigSuffix()); - $haveBusinessLicense = $configuration->get(SaferPayConfig::BUSINESS_LICENSE . SaferPayConfig::getConfigSuffix()); + // Bypassing parent::postProcess() skips PrestaShop's native permission checks, + // so state-changing actions must explicitly require 'edit' permission. + if (in_array($action, self::STATE_CHANGING_AJAX_ACTIONS) && !$this->access('edit')) { + $this->ajaxResponse(false, $this->module->l('You do not have permission to edit these settings.', self::FILE_NAME)); - if (!$haveFieldToken && $haveBusinessLicense) { - $configuration->set(SaferPayConfig::BUSINESS_LICENSE . SaferPayConfig::getConfigSuffix(), 0); - $this->errors[] = $this->module->l('Field Access Token is required to use business license'); + return false; } - $this->validateTerminalId(); + $methodName = 'ajaxProcess' . ucfirst($action); + $this->{$methodName}(); return true; } - private function validateTerminalId() + /** + * Check if current request is AJAX + */ + private function isAjax() { - try { - /** @var Configuration $configuration */ - $configuration = $this->module->getService(Configuration::class); + return (int) Tools::getValue('ajax') === 1; + } - $suffix = SaferPayConfig::getConfigSuffix(); + /** + * Validate AJAX requests come from authenticated admin + */ + private function validateAjaxToken() + { + // In PS9, the routing layer already validates the admin token in the URL + // before the controller is reached. We just verify the employee is logged in. + return $this->context->employee && $this->context->employee->id; + } + + /** + * AJAX: Save API credentials + */ + public function ajaxProcessSaveCredentials() + { + $data = $this->getJsonInput(); + if (!$data) { + $this->ajaxResponse(false, $this->module->l('Invalid request data', self::FILE_NAME)); + return; + } - $terminalId = Tools::getValue(SaferPayConfig::TERMINAL_ID . $suffix); - $customerId = Tools::getValue(SaferPayConfig::CUSTOMER_ID . $suffix) - ?: $configuration->get(SaferPayConfig::CUSTOMER_ID . $suffix); - $username = Tools::getValue(SaferPayConfig::USERNAME . $suffix) - ?: $configuration->get(SaferPayConfig::USERNAME . $suffix); - $password = Tools::getValue(SaferPayConfig::PASSWORD . $suffix) - ?: $configuration->get(SaferPayConfig::PASSWORD . $suffix); + /** @var SaferPayConfiguration $configuration */ + $configuration = $this->module->getService(SaferPayConfiguration::class); + + // Resolve active credentials for validation before saving + $isTestMode = !empty($data['testMode']); + $activeUsername = $isTestMode ? $this->getStringValue($data, 'testUsername') : $this->getStringValue($data, 'liveUsername'); + $activePassword = $isTestMode ? $this->getStringValue($data, 'testPassword') : $this->getStringValue($data, 'livePassword'); + $activeCustomerId = $this->parseCustomerIdFromUsername($activeUsername); + + if ($activePassword === self::PASSWORD_PLACEHOLDER) { + $passwordSuffix = $isTestMode ? SaferPayConfig::TEST_SUFFIX : ''; + $activePassword = (string) $configuration->get(SaferPayConfig::PASSWORD . $passwordSuffix); + } - if (empty($terminalId) || empty($customerId) || empty($username) || empty($password)) { + // Validate credentials against Saferpay API before saving + if (!empty($activeUsername) && !empty($activePassword)) { + if (empty($activeCustomerId)) { + $this->ajaxResponse(false, $this->module->l('Invalid API username. Please check your credentials and try again.', self::FILE_NAME)); return; } - $originalCustomerId = \Configuration::get(SaferPayConfig::CUSTOMER_ID . $suffix); - $originalUsername = \Configuration::get(SaferPayConfig::USERNAME . $suffix); - $originalPassword = \Configuration::get(SaferPayConfig::PASSWORD . $suffix); + try { + /** @var SaferPayGetTerminals $getTerminals */ + $getTerminals = $this->module->getService(SaferPayGetTerminals::class); + $getTerminals->fetchTerminalsWithCredentials($activeUsername, $activePassword, $activeCustomerId, $isTestMode); + } catch (\Exception $e) { + $this->ajaxResponse(false, $this->parseApiErrorMessage($e->getMessage())); + return; + } + } + + $testMerchantEmails = $this->getStringValue($data, 'testMerchantEmails'); + $liveMerchantEmails = $this->getStringValue($data, 'liveMerchantEmails'); + $invalidEmail = $this->findInvalidEmail($testMerchantEmails) ?: $this->findInvalidEmail($liveMerchantEmails); + if ($invalidEmail !== null) { + $this->ajaxResponse(false, sprintf( + $this->module->l('Invalid merchant email address: %s', self::FILE_NAME), + $invalidEmail + )); + return; + } + // Credentials validated — now save + $configuration->set(SaferPayConfig::TEST_MODE, $isTestMode ? 1 : 0); + + // Test credentials + $testUsername = $this->getStringValue($data, 'testUsername'); + $configuration->set(SaferPayConfig::USERNAME . SaferPayConfig::TEST_SUFFIX, $testUsername); + $testPassword = $this->getStringValue($data, 'testPassword'); + if ($testPassword && $testPassword !== self::PASSWORD_PLACEHOLDER) { + $configuration->set(SaferPayConfig::PASSWORD . SaferPayConfig::TEST_SUFFIX, $testPassword); + } + $configuration->set(SaferPayConfig::CUSTOMER_ID . SaferPayConfig::TEST_SUFFIX, $this->parseCustomerIdFromUsername($testUsername)); + $configuration->set(SaferPayConfig::TERMINAL_ID . SaferPayConfig::TEST_SUFFIX, $this->getStringValue($data, 'testTerminalId')); + $configuration->set(SaferPayConfig::MERCHANT_EMAILS . SaferPayConfig::TEST_SUFFIX, $this->getStringValue($data, 'testMerchantEmails')); + $configuration->set(SaferPayConfig::FIELDS_ACCESS_TOKEN . SaferPayConfig::TEST_SUFFIX, $this->getStringValue($data, 'testFieldAccessToken')); + $configuration->set(SaferPayConfig::FIELDS_LIBRARY . SaferPayConfig::TEST_SUFFIX, $this->getStringValue($data, 'testFieldJsUrl')); + + // Live credentials + $liveUsername = $this->getStringValue($data, 'liveUsername'); + $configuration->set(SaferPayConfig::USERNAME, $liveUsername); + $livePassword = $this->getStringValue($data, 'livePassword'); + if ($livePassword && $livePassword !== self::PASSWORD_PLACEHOLDER) { + $configuration->set(SaferPayConfig::PASSWORD, $livePassword); + } + $configuration->set(SaferPayConfig::CUSTOMER_ID, $this->parseCustomerIdFromUsername($liveUsername)); + $configuration->set(SaferPayConfig::TERMINAL_ID, $this->getStringValue($data, 'liveTerminalId')); + $configuration->set(SaferPayConfig::MERCHANT_EMAILS, $this->getStringValue($data, 'liveMerchantEmails')); + $configuration->set(SaferPayConfig::FIELDS_ACCESS_TOKEN, $this->getStringValue($data, 'liveFieldAccessToken')); + $configuration->set(SaferPayConfig::FIELDS_LIBRARY, $this->getStringValue($data, 'liveFieldJsUrl')); + + // Auto-detect license features from Saferpay Management API + $suffix = $isTestMode ? SaferPayConfig::TEST_SUFFIX : ''; + $hasBusinessLicense = false; + $licenseFetchFailed = false; + + if (!empty($activeUsername) && !empty($activePassword) && !empty($activeCustomerId)) { try { - \Configuration::updateValue(SaferPayConfig::CUSTOMER_ID . $suffix, $customerId); - \Configuration::updateValue(SaferPayConfig::USERNAME . $suffix, $username); - \Configuration::updateValue(SaferPayConfig::PASSWORD . $suffix, $password); + /** @var SaferPayGetLicense $getLicense */ + $getLicense = $this->module->getService(SaferPayGetLicense::class); + $licenseInfo = $getLicense->fetchLicenseWithCredentials( + $activeUsername, + $activePassword, + $activeCustomerId, + $isTestMode + ); + + $hasBusinessLicense = $licenseInfo['hasBusinessLicense']; + $configuration->set(SaferPayConfig::BUSINESS_LICENSE . $suffix, $hasBusinessLicense ? 1 : 0); + } catch (\Exception $e) { + $configuration->set(SaferPayConfig::BUSINESS_LICENSE . $suffix, 0); + $licenseFetchFailed = true; + + /** @var LoggerInterface $logger */ + $logger = $this->module->getService(LoggerInterface::class); + $logger->error('License fetch failed on credentials save: ' . $e->getMessage(), [ + 'context' => ['exception_class' => get_class($e)], + ]); + } + } else { + $configuration->set(SaferPayConfig::BUSINESS_LICENSE . $suffix, 0); + } - /** @var SaferPayTerminalService $terminalService */ - $terminalService = $this->module->getService(SaferPayTerminalService::class); + $message = $licenseFetchFailed + ? $this->module->l('Settings saved, but Saferpay Fields availability could not be confirmed. Please try again later or check the module Logs for details.', self::FILE_NAME) + : $this->module->l('Settings saved successfully.', self::FILE_NAME); + + $this->ajaxResponse( + true, + $message, + [ + 'testHasBusinessLicense' => (bool) $configuration->get(SaferPayConfig::BUSINESS_LICENSE . SaferPayConfig::TEST_SUFFIX), + 'liveHasBusinessLicense' => (bool) $configuration->get(SaferPayConfig::BUSINESS_LICENSE), + 'warning' => $licenseFetchFailed, + ] + ); + } - $terminals = $terminalService->getAvailableTerminals(); + /** + * AJAX: Save payment processing settings + */ + public function ajaxProcessSavePaymentProcessing() + { + $data = $this->getJsonInput(); + if (!$data) { + $this->ajaxResponse(false, $this->module->l('Invalid request data', self::FILE_NAME)); + return; + } - $isValid = false; - foreach ($terminals as $terminal) { - if ($terminal['TerminalId'] === $terminalId) { - $isValid = true; - break; - } - } + /** @var SaferPayConfiguration $configuration */ + $configuration = $this->module->getService(SaferPayConfiguration::class); + + $configuration->set(SaferPayConfig::PAYMENT_BEHAVIOR, $this->getIntValue($data, 'paymentBehavior')); + $configuration->set(SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D, $this->getIntValue($data, 'paymentBehaviorWithout3D')); + $configuration->set(SaferPayConfig::RESTRICT_REFUND_AMOUNT_TO_CAPTURED_AMOUNT, $this->getIntValue($data, 'restrictRefund')); + $configuration->set(SaferPayConfig::SAFERPAY_ORDER_CREATION_AFTER_AUTHORIZATION, $this->getIntValue($data, 'orderCreationAfterAuth')); + $configuration->set(SaferPayConfig::SAFERPAY_GROUP_CARDS, !empty($data['groupCards']) ? 1 : 0); + $configuration->set(SaferPayConfig::SAFERPAY_GROUP_CARDS_LOGO, !empty($data['groupCardsLogo']) ? 1 : 0); + $configuration->set(SaferPayConfig::CREDIT_CARD_SAVE, $this->getIntValue($data, 'creditCardSave')); + + // If credit card save disabled, clean up saved cards + if (empty($data['creditCardSave']) || (int) $data['creditCardSave'] === 0) { + /** @var SaferPaySavedCreditCardRepository $cardRepo */ + $cardRepo = $this->module->getService(SaferPaySavedCreditCardRepository::class); + $cardRepo->deleteAllSavedCreditCards(); + } + + $this->ajaxResponse(true, $this->module->l('Payment Processing saved successfully', self::FILE_NAME)); + } + + /** + * AJAX: Save email settings + */ + public function ajaxProcessSaveEmailSettings() + { + $data = $this->getJsonInput(); + if (!$data) { + $this->ajaxResponse(false, $this->module->l('Invalid request data', self::FILE_NAME)); + return; + } + + /** @var SaferPayConfiguration $configuration */ + $configuration = $this->module->getService(SaferPayConfiguration::class); + + $configuration->set(SaferPayConfig::SAFERPAY_ALLOW_SAFERPAY_SEND_CUSTOMER_MAIL, !empty($data['allowSaferpayMail']) ? 1 : 0); + $configuration->set(SaferPayConfig::SAFERPAY_SEND_NEW_ORDER_MAIL, !empty($data['sendNewOrderMail']) ? 1 : 0); + $configuration->set(SaferPayConfig::SAFERPAY_SEND_ORDER_CONF_MAIL, !empty($data['sendOrderConfMail']) ? 1 : 0); + + $this->ajaxResponse(true, $this->module->l('Email settings saved successfully', self::FILE_NAME)); + } + + /** + * AJAX: Save general settings + */ + public function ajaxProcessSaveGeneralSettings() + { + $data = $this->getJsonInput(); + if (!$data) { + $this->ajaxResponse(false, $this->module->l('Invalid request data', self::FILE_NAME)); + return; + } + + /** @var SaferPayConfiguration $configuration */ + $configuration = $this->module->getService(SaferPayConfiguration::class); + + $configuration->set(SaferPayConfig::SAFERPAY_ORDER_STATE_CHOICE_AWAITING_PAYMENT, $this->getIntValue($data, 'orderStateAwaitingPayment')); + $configuration->set(SaferPayConfig::SAFERPAY_PAYMENT_DESCRIPTION, $this->getStringValue($data, 'paymentDescription')); + + $configurationName = $this->getStringValue($data, 'configurationName'); + if ($configurationName !== '' && (strlen($configurationName) > 20 || !preg_match('/^[A-Za-z0-9.:\-_]+$/', $configurationName))) { + $this->ajaxResponse(false, $this->module->l('Only letters, numbers, dots, colons, hyphens, and underscores are allowed. Max 20 characters.', self::FILE_NAME)); + return; + } + $configuration->set(SaferPayConfig::CONFIGURATION_NAME, $configurationName); + $configuration->set(SaferPayConfig::SAFERPAY_ORDER_ID_OPTION, $this->getIntValue($data, 'orderIdOption')); + $configuration->set(SaferPayConfig::SAFERPAY_DEBUG_MODE, !empty($data['debugMode']) ? 1 : 0); + + $this->ajaxResponse(true, $this->module->l('General settings saved successfully', self::FILE_NAME)); + } + + /** + * AJAX: Save payment methods + */ + public function ajaxProcessSavePaymentMethods() + { + $data = $this->getJsonInput(); + if (!$data || !isset($data['paymentMethods'])) { + $this->ajaxResponse(false, $this->module->l('Invalid request data', self::FILE_NAME)); + return; + } + + // Refresh payments first + /** @var SaferPayRefreshPaymentsService $refreshPaymentsService */ + $refreshPaymentsService = $this->module->getService(SaferPayRefreshPaymentsService::class); + try { + $refreshPaymentsService->refreshPayments(); + } catch (SaferPayApiException $exception) { + $this->ajaxResponse(false, $exception->getMessage()); + return; + } + + /** @var SaferPayPaymentCreator $paymentCreation */ + $paymentCreation = $this->module->getService(SaferPayPaymentCreator::class); + + /** @var SaferPayLogoCreator $logoCreation */ + $logoCreation = $this->module->getService(SaferPayLogoCreator::class); + + /** @var SaferPayFieldCreator $fieldCreation */ + $fieldCreation = $this->module->getService(SaferPayFieldCreator::class); + + /** @var SaferPayRestrictionCreator $restrictionCreator */ + $restrictionCreator = $this->module->getService(SaferPayRestrictionCreator::class); + + $success = true; + foreach ($data['paymentMethods'] as $method) { + if (!isset($method['name']) || !is_string($method['name'])) { + continue; + } + + $paymentName = $method['name']; + $success = $paymentCreation->updatePayment($paymentName, !empty($method['enabled'])) && $success; + $success = $logoCreation->updateLogo($paymentName, !empty($method['showLogos'])) && $success; + $success = $fieldCreation->updateField($paymentName, !empty($method['showCustomForm'])) && $success; + + try { + $countries = isset($method['countries']) ? $method['countries'] : []; + $currencies = isset($method['currencies']) ? $method['currencies'] : []; - if (!$isValid && !empty($terminals)) { - $this->warnings[] = $this->module->l('Warning: The Terminal ID you entered was not found in the list of available terminals. Please verify the Terminal ID is correct.'); + if (empty($countries)) { + $countries = [SaferPayRestrictionCreator::RESTRICTION_ALL]; + } + if (empty($currencies)) { + $currencies = [SaferPayRestrictionCreator::RESTRICTION_ALL]; } - } finally { - \Configuration::updateValue(SaferPayConfig::CUSTOMER_ID . $suffix, $originalCustomerId); - \Configuration::updateValue(SaferPayConfig::USERNAME . $suffix, $originalUsername); - \Configuration::updateValue(SaferPayConfig::PASSWORD . $suffix, $originalPassword); + + $success = $restrictionCreator->updateRestriction( + $paymentName, + SaferPayRestrictionCreator::RESTRICTION_COUNTRY, + $countries + ) && $success; + $success = $restrictionCreator->updateRestriction( + $paymentName, + SaferPayRestrictionCreator::RESTRICTION_CURRENCY, + $currencies + ) && $success; + } catch (RestrictionException $e) { + $this->ajaxResponse(false, $this->module->l('Wrong restriction type', self::FILE_NAME)); + return; } - } catch (Exception $e) { - /** @var LoggerInterface $logger */ - $logger = $this->module->getService(LoggerInterface::class); - $logger->error(sprintf('%s - Failed to validate terminal ID: %s', self::FILE_NAME, $e->getMessage()), [ - 'context' => [], - 'exception' => $e, + } + + if (!$success) { + $this->ajaxResponse(false, $this->module->l('Failed to update payment methods', self::FILE_NAME)); + return; + } + + $this->ajaxResponse(true, $this->module->l('Payment methods saved successfully', self::FILE_NAME)); + } + + /** + * AJAX: Get terminals + */ + public function ajaxProcessGetTerminals() + { + $data = $this->getJsonInput(); + $isTestMode = isset($data['env']) && $data['env'] === 'test'; + $suffix = $isTestMode ? SaferPayConfig::TEST_SUFFIX : ''; + + $username = isset($data['username']) ? trim($data['username']) : ''; + $password = isset($data['password']) ? $data['password'] : ''; + $customerId = $this->parseCustomerIdFromUsername($username); + + if ($password === self::PASSWORD_PLACEHOLDER) { + /** @var SaferPayConfiguration $configuration */ + $configuration = $this->module->getService(SaferPayConfiguration::class); + $password = (string) $configuration->get(SaferPayConfig::PASSWORD . $suffix); + } + + if (empty($username) || empty($password) || empty($customerId)) { + $this->ajaxResponse(false, $this->module->l('Invalid credentials. Please check your username and password.', self::FILE_NAME)); + return; + } + + try { + /** @var SaferPayGetTerminals $getTerminals */ + $getTerminals = $this->module->getService(SaferPayGetTerminals::class); + $terminals = $getTerminals->fetchTerminalsWithCredentials($username, $password, $customerId, $isTestMode); + + $this->sendJsonResponse([ + 'success' => true, + 'terminals' => $terminals, ]); + } catch (\Exception $e) { + $this->ajaxResponse(false, $this->module->l('Invalid credentials. Please check your username and password.', self::FILE_NAME)); } } - public function initOptions() + /** + * AJAX: Generate Saferpay Fields access token + */ + public function ajaxProcessGenerateFieldAccessToken() { - $this->context->smarty->assign(SaferPayConfig::PASSWORD, SaferPayConfig::WEB_SERVICE_PASSWORD_PLACEHOLDER); - - $this->fields_options[] = $this->displayEnvironmentSelectorConfiguration(); - $this->fields_options[] = $this->displayLiveEnvironmentConfiguration(); - $this->fields_options[] = $this->displayTestEnvironmentConfiguration(); - $this->fields_options[] = $this->displayPaymentBehaviorConfiguration(); - $this->fields_options[] = $this->displayStylingConfiguration(); - $this->fields_options[] = $this->displaySavedCardsConfiguration(); - $this->fields_options[] = $this->displayEmailSettings(); - $this->fields_options[] = $this->getFieldOptionsOrderState(); - $this->fields_options[] = $this->displayConfigurationSettings(); + $data = $this->getJsonInput(); + $isTestMode = isset($data['env']) && $data['env'] === 'test'; + $suffix = $isTestMode ? SaferPayConfig::TEST_SUFFIX : ''; + + $username = isset($data['username']) ? trim($data['username']) : ''; + $password = isset($data['password']) ? $data['password'] : ''; + $terminalId = isset($data['terminalId']) ? trim($data['terminalId']) : ''; + $customerId = $this->parseCustomerIdFromUsername($username); + + if ($password === self::PASSWORD_PLACEHOLDER) { + /** @var SaferPayConfiguration $configuration */ + $configuration = $this->module->getService(SaferPayConfiguration::class); + $password = (string) $configuration->get(SaferPayConfig::PASSWORD . $suffix); + } + + if (empty($username) || empty($password) || empty($customerId) || empty($terminalId)) { + $this->ajaxResponse(false, $this->module->l('Please enter valid credentials and select a terminal first.', self::FILE_NAME)); + return; + } + + try { + /** @var SaferPayGenerateFieldAccessToken $tokenGenerator */ + $tokenGenerator = $this->module->getService(SaferPayGenerateFieldAccessToken::class); + $shopUrl = $this->context->link->getBaseLink(); + $token = $tokenGenerator->generateWithCredentials($username, $password, $customerId, $terminalId, $isTestMode, $shopUrl); + + /** @var SaferPayConfiguration $configuration */ + $configuration = $this->module->getService(SaferPayConfiguration::class); + $configuration->set(SaferPayConfig::FIELDS_ACCESS_TOKEN . $suffix, $token); + + $this->sendJsonResponse([ + 'success' => true, + 'message' => $this->module->l('Access token generated successfully.', self::FILE_NAME), + 'token' => $token, + ]); + } catch (\Exception $e) { + \PrestaShopLogger::addLog( + 'SaferPay: Failed to generate field access token - ' . $e->getMessage(), + 3, + null, + null, + null, + true + ); + + $this->ajaxResponse(false, $this->module->l('Failed to generate access token.', self::FILE_NAME)); + } } /** - * @param $isNewTheme - * @return void + * AJAX: Refresh all data */ - public function setMedia($isNewTheme = false) + public function ajaxProcessRefreshData() { - parent::setMedia($isNewTheme); + $settingsData = $this->collectSettingsData(); + $this->sendJsonResponse([ + 'success' => true, + 'data' => $settingsData, + ]); + } + + /** + * Collect all settings data to pass to the React app + */ + private function collectSettingsData() + { + /** @var SaferPayConfiguration $configuration */ + $configuration = $this->module->getService(SaferPayConfiguration::class); + + // Resolved before the payload is built because it sets $paymentMethodsFetchFailed. + $paymentMethodsData = $this->getPaymentMethodsData(); + + $data = [ + // Environment + 'testMode' => (bool) $configuration->get(SaferPayConfig::TEST_MODE), + + // Test credentials + 'testUsername' => (string) $configuration->get(SaferPayConfig::USERNAME . SaferPayConfig::TEST_SUFFIX), + 'testPassword' => $configuration->get(SaferPayConfig::PASSWORD . SaferPayConfig::TEST_SUFFIX) ? self::PASSWORD_PLACEHOLDER : '', + 'testTerminalId' => (string) $configuration->get(SaferPayConfig::TERMINAL_ID . SaferPayConfig::TEST_SUFFIX), + 'testMerchantEmails' => (string) $configuration->get(SaferPayConfig::MERCHANT_EMAILS . SaferPayConfig::TEST_SUFFIX) ?: (string) \Configuration::get('PS_SHOP_EMAIL'), + 'testFieldAccessToken' => (string) $configuration->get(SaferPayConfig::FIELDS_ACCESS_TOKEN . SaferPayConfig::TEST_SUFFIX), + 'testFieldJsUrl' => (string) $configuration->get(SaferPayConfig::FIELDS_LIBRARY . SaferPayConfig::TEST_SUFFIX), + + // Live credentials + 'liveUsername' => (string) $configuration->get(SaferPayConfig::USERNAME), + 'livePassword' => $configuration->get(SaferPayConfig::PASSWORD) ? self::PASSWORD_PLACEHOLDER : '', + 'liveTerminalId' => (string) $configuration->get(SaferPayConfig::TERMINAL_ID), + 'liveMerchantEmails' => (string) $configuration->get(SaferPayConfig::MERCHANT_EMAILS) ?: (string) \Configuration::get('PS_SHOP_EMAIL'), + 'liveFieldAccessToken' => (string) $configuration->get(SaferPayConfig::FIELDS_ACCESS_TOKEN), + 'liveFieldJsUrl' => (string) $configuration->get(SaferPayConfig::FIELDS_LIBRARY), + + // License (auto-detected, per environment) + 'testHasBusinessLicense' => (bool) $configuration->get(SaferPayConfig::BUSINESS_LICENSE . SaferPayConfig::TEST_SUFFIX), + 'liveHasBusinessLicense' => (bool) $configuration->get(SaferPayConfig::BUSINESS_LICENSE), + + // Payment Processing + 'paymentBehavior' => (int) $configuration->get(SaferPayConfig::PAYMENT_BEHAVIOR), + 'paymentBehaviorWithout3D' => (int) $configuration->get(SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D), + 'restrictRefund' => (int) $configuration->get(SaferPayConfig::RESTRICT_REFUND_AMOUNT_TO_CAPTURED_AMOUNT), + 'orderCreationAfterAuth' => (int) $configuration->get(SaferPayConfig::SAFERPAY_ORDER_CREATION_AFTER_AUTHORIZATION), + 'groupCards' => (bool) $configuration->get(SaferPayConfig::SAFERPAY_GROUP_CARDS), + 'groupCardsLogo' => (bool) $configuration->get(SaferPayConfig::SAFERPAY_GROUP_CARDS_LOGO), + 'creditCardSave' => (int) $configuration->get(SaferPayConfig::CREDIT_CARD_SAVE), + + // Email + 'allowSaferpayMail' => (bool) $configuration->get(SaferPayConfig::SAFERPAY_ALLOW_SAFERPAY_SEND_CUSTOMER_MAIL), + 'sendNewOrderMail' => (bool) $configuration->get(SaferPayConfig::SAFERPAY_SEND_NEW_ORDER_MAIL), + 'sendOrderConfMail' => (bool) $configuration->get(SaferPayConfig::SAFERPAY_SEND_ORDER_CONF_MAIL), + + // General + 'orderStateAwaitingPayment' => (int) $configuration->get(SaferPayConfig::SAFERPAY_ORDER_STATE_CHOICE_AWAITING_PAYMENT), + 'paymentDescription' => (string) $configuration->get(SaferPayConfig::SAFERPAY_PAYMENT_DESCRIPTION), + 'configurationName' => (string) $configuration->get(SaferPayConfig::CONFIGURATION_NAME), + 'orderIdOption' => (int) $configuration->get(SaferPayConfig::SAFERPAY_ORDER_ID_OPTION), + 'debugMode' => (bool) $configuration->get(SaferPayConfig::SAFERPAY_DEBUG_MODE), + + // Reference data + 'orderStates' => $this->getOrderStates(), + 'countries' => $this->getCountries(), + 'currencies' => $this->getCurrencies(), + 'paymentMethods' => $paymentMethodsData, + 'paymentMethodsFetchFailed' => $this->paymentMethodsFetchFailed, + + // Endpoints + 'ajaxUrl' => $this->context->link->getAdminLink('AdminSaferPayOfficialSettings'), + 'adminToken' => Tools::getAdminTokenLite('AdminSaferPayOfficialSettings'), + + // Translations + 'translations' => $this->getSettingsTranslations(), + ]; + + return $data; + } + + /** + * Get all translatable strings for the React frontend + */ + private function getSettingsTranslations() + { + /** @var \Invertus\SaferPay\Service\SettingsTranslationService $translationService */ + $translationService = $this->module->getService(\Invertus\SaferPay\Service\SettingsTranslationService::class); - $this->addJS('modules/' . $this->module->name . '/views/js/admin/saferpay_settings.js'); + return $translationService->getAll(); } /** - * @param string $environment 'test' or 'live' - * @return array + * Get order states for dropdown */ - private function getTerminalsForEnvironment($environment = 'live') + private function getOrderStates() { - $suffix = ($environment === 'test') ? SaferPayConfig::TEST_SUFFIX : ''; + $states = OrderState::getOrderStates($this->context->language->id); + $result = []; + foreach ($states as $state) { + $result[] = [ + 'id' => (int) $state['id_order_state'], + 'name' => $state['name'], + ]; + } + return $result; + } - $customerId = Tools::getValue(SaferPayConfig::CUSTOMER_ID . $suffix) - ?: \Configuration::get(SaferPayConfig::CUSTOMER_ID . $suffix); - $username = Tools::getValue(SaferPayConfig::USERNAME . $suffix) - ?: \Configuration::get(SaferPayConfig::USERNAME . $suffix); - $password = Tools::getValue(SaferPayConfig::PASSWORD . $suffix) - ?: \Configuration::get(SaferPayConfig::PASSWORD . $suffix); + /** + * Get active countries + */ + private function getCountries() + { + $countries = Country::getCountries($this->context->language->id, true); + $result = []; + $result[] = ['id' => 0, 'name' => $this->module->l('All', self::FILE_NAME)]; + foreach ($countries as $key => $country) { + $result[] = [ + 'id' => (int) $key, + 'name' => $country['name'], + ]; + } + return $result; + } - if (empty($customerId) || empty($username) || empty($password)) { - return []; + /** + * Get active currencies + */ + private function getCurrencies() + { + $currencies = Currency::getCurrencies(); + $result = []; + $result[] = ['id' => 0, 'iso_code' => $this->module->l('All', self::FILE_NAME)]; + foreach ($currencies as $currency) { + $result[] = [ + 'id' => (int) $currency['id_currency'], + 'iso_code' => $currency['iso_code'], + ]; } + return $result; + } + + /** + * Get payment methods data with their current state + */ + private function getPaymentMethodsData() + { + /** @var SaferPayPaymentRepository $paymentRepository */ + $paymentRepository = $this->module->getService(SaferPayPaymentRepository::class); - $originalCustomerId = \Configuration::get(SaferPayConfig::CUSTOMER_ID . $suffix); - $originalUsername = \Configuration::get(SaferPayConfig::USERNAME . $suffix); - $originalPassword = \Configuration::get(SaferPayConfig::PASSWORD . $suffix); - $originalTestMode = \Configuration::get(SaferPayConfig::TEST_MODE); + // A fresh install has no credentials yet, so calling the account would fail and + // write a misleading error to the merchant's log. Skip the account entirely until + // the credentials needed to build the request are present. + $hasCredentials = $this->hasApiCredentials(); try { - \Configuration::updateValue(SaferPayConfig::CUSTOMER_ID . $suffix, $customerId); - \Configuration::updateValue(SaferPayConfig::USERNAME . $suffix, $username); - \Configuration::updateValue(SaferPayConfig::PASSWORD . $suffix, $password); - \Configuration::updateValue(SaferPayConfig::TEST_MODE, $environment === 'test' ? 1 : 0); + // Re-read the account and reconcile the stored list when the Payment Methods + // settings open, so methods added/removed on the Saferpay account are reflected + // (and persisted for the front office) without requiring a Save click. Enabled + // flags are preserved by the refresh; newly added methods default to disabled. + if ($hasCredentials) { + /** @var SaferPayRefreshPaymentsService $refreshPaymentsService */ + $refreshPaymentsService = $this->module->getService(SaferPayRefreshPaymentsService::class); + $refreshPaymentsService->refreshPayments(); + } + + // The refresh persists the account's methods, so read them back from storage + // instead of calling the API a second time. + $paymentMethods = array_column($paymentRepository->getAllPaymentMethodsNames(), 'name'); + + // refreshPayments() is a no-op when nothing is active yet (e.g. a fresh setup), + // so fall back to the live account list to still surface newly available methods. + if (empty($paymentMethods) && $hasCredentials) { + /** @var SaferPayObtainPaymentMethods $obtainMethods */ + $obtainMethods = $this->module->getService(SaferPayObtainPaymentMethods::class); + $paymentMethods = $obtainMethods->obtainPaymentMethodsNamesAsArray(); + } + } catch (SaferPayApiException $exception) { + // Account unreachable (bad credentials / offline): keep the last-known stored + // list rather than wiping the page. Credential validity is surfaced separately + // on the Credentials tab. Never clears stored configuration. + $paymentMethods = array_column($paymentRepository->getAllPaymentMethodsNames(), 'name'); - /** @var SaferPayTerminalService $terminalService */ - $terminalService = $this->module->getService(SaferPayTerminalService::class); - $terminals = $terminalService->getAvailableTerminals($customerId); + $this->paymentMethodsFetchFailed = true; - return $terminals; - } catch (Exception $e) { /** @var LoggerInterface $logger */ $logger = $this->module->getService(LoggerInterface::class); - $logger->error(sprintf('%s - Failed to get terminals: %s', self::FILE_NAME, $e->getMessage()), [ - 'context' => [], - 'exception' => $e, + $logger->error('Failed to refresh payment methods from Saferpay account: ' . $exception->getMessage(), [ + 'context' => ['exception_class' => get_class($exception)], ]); - return []; - } finally { - \Configuration::updateValue(SaferPayConfig::CUSTOMER_ID . $suffix, $originalCustomerId); - \Configuration::updateValue(SaferPayConfig::USERNAME . $suffix, $originalUsername); - \Configuration::updateValue(SaferPayConfig::PASSWORD . $suffix, $originalPassword); - \Configuration::updateValue(SaferPayConfig::TEST_MODE, $originalTestMode); } + + /** @var SaferPayLogoRepository $logoRepository */ + $logoRepository = $this->module->getService(SaferPayLogoRepository::class); + + /** @var SaferPayFieldRepository $fieldRepository */ + $fieldRepository = $this->module->getService(SaferPayFieldRepository::class); + + /** @var SaferPayRestrictionRepository $restrictionRepository */ + $restrictionRepository = $this->module->getService(SaferPayRestrictionRepository::class); + + /** @var SaferPayPaymentNotation $saferPayPaymentNotation */ + $saferPayPaymentNotation = $this->module->getService(SaferPayPaymentNotation::class); + + $result = []; + foreach ($paymentMethods as $paymentMethod) { + $result[] = [ + 'name' => $paymentMethod, + 'displayName' => $saferPayPaymentNotation->getForDisplay($paymentMethod), + 'enabled' => (bool) $paymentRepository->isActiveByName($paymentMethod), + 'showLogos' => (bool) $logoRepository->isActiveByName($paymentMethod), + 'showCustomForm' => (bool) $fieldRepository->isActiveByName($paymentMethod), + 'hasCustomForm' => in_array($paymentMethod, SaferPayConfig::FIELD_SUPPORTED_PAYMENT_METHODS), + 'countries' => $restrictionRepository->getSelectedIdsByName( + $paymentMethod, + SaferPayRestrictionCreator::RESTRICTION_COUNTRY + ), + 'currencies' => $restrictionRepository->getSelectedIdsByName( + $paymentMethod, + SaferPayRestrictionCreator::RESTRICTION_CURRENCY + ), + ]; + } + + return $result; } /** - * @return array + * Whether every credential the payment methods request is built from is configured + * for the active environment. + * + * @return bool */ - private function getFieldOptionsOrderState() + private function hasApiCredentials() { - return [ - 'title' => $this->module->l('Order state'), - 'fields' => [ - SaferPayConfig::SAFERPAY_ORDER_STATE_CHOICE_AWAITING_PAYMENT => [ - 'title' => $this->module->l( - sprintf( - 'Status for %s', - Tools::ucfirst(Tools::strtolower(SaferPayConfig::SAFERPAY_PAYMENT_AWAITING)) - ) - ), - 'required' => false, - 'cast' => 'intval', - 'type' => 'select', - 'list' => OrderState::getOrderStates($this->context->language->id), - 'identifier' => 'id_order_state', - 'desc' => 'Default status on SaferPay order creation', - ], - ], - 'buttons' => [ - 'save_and_connect' => [ - 'title' => $this->module->l('Save'), - 'icon' => 'process-icon-save', - 'class' => 'btn btn-default pull-right', - 'type' => 'submit', - ], - ], + $suffix = SaferPayConfig::getConfigSuffix(); + $required = [ + SaferPayConfig::USERNAME, + SaferPayConfig::PASSWORD, + SaferPayConfig::CUSTOMER_ID, + SaferPayConfig::TERMINAL_ID, ]; + + foreach ($required as $key) { + if (!Configuration::get($key . $suffix)) { + return false; + } + } + + return true; } /** - * @return array + * Get JSON input from request body */ - private function displayConfigurationSettings() + private function getJsonInput() { - return [ - 'title' => $this->module->l('Configuration', self::FILE_NAME), - 'fields' => [ - SaferPayConfig::SAFERPAY_PAYMENT_DESCRIPTION => [ - 'title' => $this->module->l('Description', self::FILE_NAME), - 'type' => 'text', - 'desc' => 'This description is visible in payment page also in payment confirmation email', - 'class' => 'fixed-width-xxl', - ], - SaferPayConfig::SAFERPAY_DEBUG_MODE => [ - 'title' => $this->module->l('Debug mode', self::FILE_NAME), - 'validation' => 'isBool', - 'cast' => 'intval', - 'type' => 'bool', - 'desc' => $this->module->l('Enable debug mode to see more information in logs', self::FILE_NAME), - ], - ], - 'buttons' => [ - 'save_and_connect' => [ - 'title' => $this->module->l('Save', self::FILE_NAME), - 'icon' => 'process-icon-save', - 'class' => 'btn btn-default pull-right', - 'type' => 'submit', - ], - ], - ]; + $raw = file_get_contents('php://input'); + $data = json_decode($raw, true); + return is_array($data) ? $data : null; } /** - * @return array + * Send AJAX JSON response */ - private function displaySavedCardsConfiguration() + private function ajaxResponse($success, $message = '', $extraData = []) { - return [ - 'title' => $this->module->l('Credit card saving'), - 'icon' => 'icon-settings', - 'fields' => [ - SaferPayConfig::CREDIT_CARD_SAVE => [ - 'type' => 'radio', - 'title' => $this->module->l('Credit card saving for customers'), - 'validation' => 'isInt', - 'choices' => [ - 1 => $this->module->l('Enable'), - 0 => $this->module->l('Disable'), - ], - 'desc' => $this->module->l('Allow customers to save credit card for faster purchase'), - 'form_group_class' => 'thumbs_chose', - ], - ], - 'buttons' => [ - 'save_and_connect' => [ - 'title' => $this->module->l('Save'), - 'icon' => 'process-icon-save', - 'class' => 'btn btn-default pull-right', - 'type' => 'submit', - ], - ], - ]; + $this->sendJsonResponse(array_merge([ + 'success' => $success, + 'message' => $message, + ], $extraData)); } /** - * @return array + * Send JSON response and terminate (PS9 compatible) */ - private function displayStylingConfiguration() + private function sendJsonResponse(array $data) { - return [ - 'title' => $this->module->l('Styling'), - 'icon' => 'icon-settings', - 'fields' => [ - SaferPayConfig::CONFIGURATION_NAME => [ - 'title' => $this->module->l('Payment Page configurations name'), - 'type' => 'text', - 'class' => 'fixed-width-xl', - ], - ], - 'buttons' => [ - 'save_and_connect' => [ - 'title' => $this->module->l('Save'), - 'icon' => 'process-icon-save', - 'class' => 'btn btn-default pull-right', - 'type' => 'submit', - ], - ], - ]; + header('Content-Type: application/json'); + ob_end_clean(); + die(json_encode($data)); } /** - * @return array + * Parse customer ID from API username (format: PREFIX_CUSTOMERID_SUFFIX) */ - private function displayEmailSettings() + private function parseCustomerIdFromUsername($username) { - return [ - 'title' => $this->module->l('Email sending'), - 'icon' => 'icon-settings', - 'fields' => [ - SaferPayConfig::SAFERPAY_ALLOW_SAFERPAY_SEND_CUSTOMER_MAIL => [ - 'title' => $this->module->l('Send an email from Saferpay on payment completion'), - 'desc' => $this->module->l('With this setting enabled an email from the Saferpay system will be sent to the customer'), - 'validation' => 'isBool', - 'cast' => 'intval', - 'type' => 'bool', - ], - SaferPayConfig::SAFERPAY_SEND_NEW_ORDER_MAIL => [ - 'title' => $this->module->l('Send new order mail on authorization'), - 'desc' => $this->module->l('Receive a notification when an order is authorized by Saferpay (Using the Mail alert module)'), - 'validation' => 'isBool', - 'cast' => 'intval', - 'type' => 'bool', - ], - SaferPayConfig::SAFERPAY_SEND_ORDER_CONF_MAIL => [ - 'title' => $this->module->l('Send order confirmation mail on payment completion'), - 'desc' => $this->module->l('Send an email from Saferpay on payment completion'), - 'validation' => 'isBool', - 'cast' => 'intval', - 'type' => 'bool', - ], - SaferPayConfig::SAFERPAY_SEND_NEW_ORDER_MAIL . '_description' => [ - 'type' => 'desc', - 'class' => 'col-lg-12', - 'template' => 'field-new-order-mail-desc.tpl', - ], - ], - 'buttons' => [ - 'save_and_connect' => [ - 'title' => $this->module->l('Save'), - 'icon' => 'process-icon-save', - 'class' => 'btn btn-default pull-right', - 'type' => 'submit', - ], - ], - ]; + $parts = explode('_', (string) $username); + + return isset($parts[1]) ? $parts[1] : ''; } /** - * @return array + * Parse Saferpay API error message into user-friendly text */ - private function displayPaymentBehaviorConfiguration() + private function parseApiErrorMessage($rawMessage) { - return [ - 'title' => $this->module->l('Payment behavior'), - 'icon' => 'icon-settings', - 'fields' => [ - SaferPayConfig::PAYMENT_BEHAVIOR => [ - 'type' => 'radio', - 'title' => $this->module->l('Default payment behavior'), - 'validation' => 'isInt', - 'choices' => [ - 0 => $this->module->l('Capture'), - 1 => $this->module->l('Authorize'), - ], - 'desc' => $this->module->l('How payment provider should behave when order is created'), - 'form_group_class' => 'thumbs_chose', - ], - SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D => [ - 'type' => 'radio', - 'title' => $this->module->l('Behaviour when 3D secure fails'), - 'validation' => 'isInt', - 'choices' => [ - SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CANCEL => $this->module->l('Cancel'), - SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_AUTHORIZE => $this->module->l('Authorize'), - ], - 'desc' => $this->module->l('Default payment behavior for payment without 3-D Secure'), - 'form_group_class' => 'thumbs_chose', - ], - SaferPayConfig::RESTRICT_REFUND_AMOUNT_TO_CAPTURED_AMOUNT => [ - 'type' => 'radio', - 'title' => $this->module->l('Restrict RefundAmount To Captured Amount'), - 'validation' => 'isInt', - 'choices' => [ - 1 => $this->module->l('Enable'), - 0 => $this->module->l('Disable'), - ], - 'desc' => $this->module->l('If set to true, the refund will be rejected if the sum of authorized refunds exceeds the capture value.'), - 'form_group_class' => 'thumbs_chose', - ], - SaferPayConfig::SAFERPAY_ORDER_CREATION_AFTER_AUTHORIZATION => [ - 'type' => 'radio', - 'title' => $this->module->l('Order creation rule'), - 'validation' => 'isInt', - 'choices' => [ - 1 => $this->module->l('After authorization'), - 0 => $this->module->l('Before authorization'), - ], - 'desc' => $this->module->l('Select the option to determine whether the order should be created'), - 'form_group_class' => 'thumbs_chose', - ], - SaferPayConfig::SAFERPAY_GROUP_CARDS => [ - 'type' => 'bool', - 'title' => $this->module->l("Group debit/credit cards as 'Cards' in checkout", self::FILE_NAME), - 'validation' => 'isBool', - 'cast' => 'intval', - 'desc' => $this->module->l("If enabled, all supported card brands (Visa, Mastercard, Amex, etc.) will be grouped and shown as a single 'Cards' payment method at checkout.", self::FILE_NAME), - ], - SaferPayConfig::SAFERPAY_GROUP_CARDS_LOGO => [ - 'type' => 'bool', - 'title' => $this->module->l("Show 'Cards' payment method logo", self::FILE_NAME), - 'validation' => 'isBool', - 'cast' => 'intval', - 'desc' => $this->module->l("If enabled, a logo for the grouped 'Cards' payment method will be displayed at checkout.", self::FILE_NAME), - ], - ], - 'buttons' => [ - 'save_and_connect' => [ - 'title' => $this->module->l('Save'), - 'icon' => 'process-icon-save', - 'class' => 'btn btn-default pull-right', - 'type' => 'submit', - ], - ], - ]; + $jsonStart = strpos($rawMessage, '{'); + if ($jsonStart !== false) { + $jsonString = substr($rawMessage, $jsonStart); + $decoded = json_decode($jsonString, true); + if (is_array($decoded) && !empty($decoded['ErrorName'])) { + $errorName = $decoded['ErrorName']; + if ($errorName === 'AUTHENTICATION_FAILED') { + return $this->module->l('Invalid API credentials. Please verify your username and password.', self::FILE_NAME); + } + + $message = $this->module->l('API validation failed:', self::FILE_NAME) . ' ' . $errorName; + if (!empty($decoded['ErrorMessage'])) { + $message .= ' — ' . $decoded['ErrorMessage']; + } + + return $message; + } + } + + return $this->module->l('API validation failed. Please check your credentials and try again.', self::FILE_NAME); } /** - * @return array + * Get string value from data array */ - private function displayTestEnvironmentConfiguration() + private function getStringValue($data, $key) { - return [ - 'title' => $this->module->l('Test environment'), - 'icon' => 'icon-settings', - 'fields' => [ - SaferPayConfig::USERNAME . SaferPayConfig::TEST_SUFFIX => [ - 'title' => $this->module->l('JSON API Username'), - 'type' => 'text', - 'validation' => 'isGenericName', - 'class' => 'fixed-width-xl', - ], - SaferPayConfig::PASSWORD . SaferPayConfig::TEST_SUFFIX => [ - 'title' => $this->module->l('JSON API Password'), - 'type' => 'password_input', - 'class' => 'fixed-width-xl', - 'value' => \Configuration::get(SaferPayConfig::PASSWORD . SaferPayConfig::TEST_SUFFIX), - ], - SaferPayConfig::CUSTOMER_ID . SaferPayConfig::TEST_SUFFIX => [ - 'title' => $this->module->l('Customer ID'), - 'type' => 'text', - 'class' => 'fixed-width-xl', - 'size' => 3, - ], - SaferPayConfig::TERMINAL_ID . SaferPayConfig::TEST_SUFFIX => [ - 'title' => $this->module->l('Terminal ID'), - 'type' => 'terminal_selector', - 'class' => 'fixed-width-xl', - 'value' => \Configuration::get(SaferPayConfig::TERMINAL_ID . SaferPayConfig::TEST_SUFFIX), - 'environment' => 'test', - 'terminals' => $this->getTerminalsForEnvironment('test'), - ], - SaferPayConfig::MERCHANT_EMAILS . SaferPayConfig::TEST_SUFFIX => [ - 'title' => $this->module->l('Merchant emails'), - 'type' => 'text', - 'class' => 'fixed-width-xl', - ], - SaferPayConfig::FIELDS_ACCESS_TOKEN . SaferPayConfig::TEST_SUFFIX . '_description' => [ - 'type' => 'desc', - 'class' => 'col-lg-12', - 'template' => 'field-access-token-desc.tpl', - ], - SaferPayConfig::FIELDS_ACCESS_TOKEN . SaferPayConfig::TEST_SUFFIX => [ - 'title' => $this->module->l('Field Access Token'), - 'type' => 'text', - 'class' => 'fixed-width-xxl', - ], - SaferPayConfig::BUSINESS_LICENSE . SaferPayConfig::TEST_SUFFIX => [ - 'title' => $this->module->l('I have Business license'), - 'validation' => 'isBool', - 'cast' => 'intval', - 'type' => 'bool', - ], - ], - 'buttons' => [ - 'save_and_connect' => [ - 'title' => $this->module->l('Save'), - 'icon' => 'process-icon-save', - 'class' => 'btn btn-default pull-right', - 'type' => 'submit', - ], - ], - ]; + return isset($data[$key]) ? (string) $data[$key] : ''; } /** - * @return array + * Get int value from data array */ - private function displayLiveEnvironmentConfiguration() + private function getIntValue($data, $key) { - return [ - 'title' => $this->module->l('Live environment'), - 'icon' => 'icon-settings', - 'fields' => [ - SaferPayConfig::USERNAME => [ - 'title' => $this->module->l('JSON API Username'), - 'type' => 'text', - 'validation' => 'isGenericName', - 'class' => 'fixed-width-xl', - ], - SaferPayConfig::PASSWORD => [ - 'title' => $this->module->l('JSON API Password'), - 'type' => 'password_input', - 'class' => 'fixed-width-xl', - 'value' => \Configuration::get(SaferPayConfig::PASSWORD), - ], - SaferPayConfig::CUSTOMER_ID => [ - 'title' => $this->module->l('Customer ID'), - 'type' => 'text', - 'class' => 'fixed-width-xl', - 'size' => 3, - ], - SaferPayConfig::TERMINAL_ID => [ - 'title' => $this->module->l('Terminal ID'), - 'type' => 'terminal_selector', - 'class' => 'fixed-width-xl', - 'value' => \Configuration::get(SaferPayConfig::TERMINAL_ID), - 'environment' => 'live', - 'terminals' => $this->getTerminalsForEnvironment('live'), - ], - SaferPayConfig::MERCHANT_EMAILS => [ - 'title' => $this->module->l('Merchant emails'), - 'type' => 'text', - 'class' => 'fixed-width-xl', - ], - SaferPayConfig::FIELDS_ACCESS_TOKEN . '_description' => [ - 'type' => 'desc', - 'class' => 'col-lg-12', - 'template' => 'field-access-token-desc.tpl', - ], - SaferPayConfig::FIELDS_ACCESS_TOKEN => [ - 'title' => $this->module->l('Field Access Token'), - 'type' => 'text', - 'class' => 'fixed-width-xxl', - ], - SaferPayConfig::BUSINESS_LICENSE => [ - 'title' => $this->module->l('I have Business license'), - 'validation' => 'isBool', - 'cast' => 'intval', - 'type' => 'bool', - ], - ], - 'buttons' => [ - 'save_and_connect' => [ - 'title' => $this->module->l('Save'), - 'icon' => 'process-icon-save', - 'class' => 'btn btn-default pull-right', - 'type' => 'submit', - ], - ], - ]; + return isset($data[$key]) ? (int) $data[$key] : 0; } /** - * @return array + * Returns the first invalid email in a comma-separated list, or null if all are valid. */ - private function displayEnvironmentSelectorConfiguration() + private function findInvalidEmail($emails) { - return [ - 'title' => $this->module->l('Select environment'), - 'icon' => 'icon-settings', - 'fields' => [ - SaferPayConfig::TEST_MODE => [ - 'title' => $this->module->l('Test mode'), - 'validation' => 'isBool', - 'cast' => 'intval', - 'type' => 'bool', - ], - ], - 'buttons' => [ - 'save_and_connect' => [ - 'title' => $this->module->l('Save'), - 'icon' => 'process-icon-save', - 'class' => 'btn btn-default pull-right', - 'type' => 'submit', - ], - ], - ]; + if ($emails === '') { + return null; + } + foreach (explode(',', $emails) as $email) { + $email = trim($email); + if ($email === '') { + continue; + } + if (!\Validate::isEmail($email)) { + return $email; + } + } + return null; } } diff --git a/controllers/front/ajax.php b/controllers/front/ajax.php index e26b30dc4..fb7b9ef64 100644 --- a/controllers/front/ajax.php +++ b/controllers/front/ajax.php @@ -134,17 +134,11 @@ private function getFailControllerLink($cartId, $secureKey, $moduleId) private function getSuccessControllerName($isBusinessLicence, $fieldToken) { - $successController = ControllerName::SUCCESS; - - if ($isBusinessLicence) { - $successController = ControllerName::SUCCESS_IFRAME; - } - if ($fieldToken) { - $successController = ControllerName::SUCCESS_HOSTED; + return ControllerName::SUCCESS_HOSTED; } - return $successController; + return ControllerName::SUCCESS; } private function submitHostedFields() diff --git a/controllers/front/fail.php b/controllers/front/fail.php index 6161fbba5..ab47fa362 100644 --- a/controllers/front/fail.php +++ b/controllers/front/fail.php @@ -21,8 +21,10 @@ *@license SIX Payment Services */ +use Invertus\SaferPay\Config\SaferPayConfig; use Invertus\SaferPay\Controller\AbstractSaferPayController; use Invertus\SaferPay\Factory\OrderPresenterFactory; +use Invertus\SaferPay\Repository\SaferPayOrderRepository; use Invertus\SaferPay\Service\CartDuplicationService; use Invertus\SaferPay\Logger\LoggerInterface; @@ -84,6 +86,8 @@ public function initContent() $logger->debug(sprintf('%s - Controller called', self::FILE_NAME)); + $this->markOrderAsFailed($logger); + $this->warning[] = $this->module->l('We couldn\'t authorize your payment. Please try again.', self::FILE_NAME); $logger->debug(sprintf('%s - Controller action ended', self::FILE_NAME)); @@ -102,4 +106,79 @@ public function initContent() ) ); } + + /** + * When the "Order creation rule" is "Before authorization", the order row already exists by the time + * the customer lands here after a rejected/failed authorization. Transition it to the failed state and + * flag the Saferpay order as canceled so it does not stay stuck on "Awaiting Saferpay payment" forever + * (and so the awaiting-status poller stops spinning). Mirrors the failure handling in notify.php. + * + * @param LoggerInterface $logger + * + * @return void + */ + private function markOrderAsFailed($logger) + { + /** @var SaferPayOrderRepository $orderRepo */ + $orderRepo = $this->module->getService(SaferPayOrderRepository::class); + + $saferPayOrderId = (int) $orderRepo->getIdByCartId($this->id_cart); + + if (!$saferPayOrderId) { + // "After authorization" mode: no order was created for a failed payment, nothing to update. + return; + } + + $saferPayOrder = new SaferPayOrder($saferPayOrderId); + + if (!Validate::isLoadedObject($saferPayOrder)) { + return; + } + + // Payment already succeeded through another path (e.g. the notify webhook); never override it. + if ($saferPayOrder->authorized || $saferPayOrder->captured) { + return; + } + + $orderId = (int) Order::getIdByCartId($this->id_cart); + $failedStatus = (int) _SAFERPAY_PAYMENT_AUTHORIZATION_FAILED_; + + if ($orderId && $failedStatus) { + $order = new Order($orderId); + + if (Validate::isLoadedObject($order)) { + $currentState = (int) $order->current_state; + + $authorizedStatus = (int) Configuration::get(SaferPayConfig::SAFERPAY_PAYMENT_AUTHORIZED); + $capturedStatus = (int) Configuration::get(SaferPayConfig::SAFERPAY_PAYMENT_COMPLETED); + + // Do not override a success state, and avoid duplicate history entries if already failed. + if ($currentState !== $authorizedStatus + && $currentState !== $capturedStatus + && $currentState !== $failedStatus + ) { + $order->setCurrentState($failedStatus); + + $logger->debug(sprintf('%s - Order transitioned to authorization failed', self::FILE_NAME), [ + 'context' => [ + 'id_order' => $orderId, + 'id_cart' => $this->id_cart, + ], + ]); + } + } + } + + if (!$saferPayOrder->canceled) { + $saferPayOrder->authorized = false; + $saferPayOrder->pending = false; + $saferPayOrder->canceled = true; + + if ($orderId) { + $saferPayOrder->id_order = $orderId; + } + + $saferPayOrder->update(); + } + } } diff --git a/controllers/front/failIFrame.php b/controllers/front/failIFrame.php deleted file mode 100644 index 1cc08c65b..000000000 --- a/controllers/front/failIFrame.php +++ /dev/null @@ -1,107 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ - -use Invertus\SaferPay\Config\SaferPayConfig; -use Invertus\SaferPay\Controller\AbstractSaferPayController; -use Invertus\SaferPay\Enum\ControllerName; -use Invertus\SaferPay\Logger\LoggerInterface; - -if (!defined('_PS_VERSION_')) { - exit; -} - -class SaferPayOfficialFailIFrameModuleFrontController extends AbstractSaferPayController -{ - const FILE_NAME = 'failIFrame'; - - protected $display_header = false; - protected $display_footer = false; - - public function init() - { - $this->display_header = true; - - parent::init(); - } - - public function initContent() - { - parent::initContent(); - - $cart = new \Cart(Tools::getValue('cartId')); - - /** - * Note: deleting cart prevents - * from further failing when creating order with same cart - */ - $cart->delete(); - - /** @var LoggerInterface $logger */ - $logger = $this->module->getService(LoggerInterface::class); - - $logger->debug(sprintf('%s - Controller called', self::FILE_NAME)); - - $logger->debug(sprintf('%s - Controller action ended', self::FILE_NAME)); - - - $this->setTemplate(SaferPayConfig::SAFERPAY_TEMPLATE_LOCATION . '/front/loading.tpl'); - } - - public function setMedia() - { - parent::setMedia(); - - $cartId = Tools::getValue('cartId'); - $moduleId = Tools::getValue('moduleId'); - $orderId = Tools::getValue('orderId'); - $secureKey = Tools::getValue('secureKey'); - - $failUrl = $this->context->link->getModuleLink( - $this->module->name, - ControllerName::FAIL, - [ - 'cartId' => $cartId, - 'secureKey' => $secureKey, - 'orderId' => $orderId, - 'moduleId' => $moduleId, - ], - true - ); - - $this->context->controller->registerStylesheet( - $this->module->name . '-iframe-css', - 'modules/' . $this->module->name . '/views/css/front/loading.css' - ); - - Media::addJsDef([ - 'redirectUrl' => $failUrl, - ]); - - $this->context->controller->registerJavascript( - $this->module->name . '-iframe-js', - '/modules/' . $this->module->name . '/views/js/front/saferpay_iframe.js' - ); - - return true; - } -} diff --git a/controllers/front/failValidation.php b/controllers/front/failValidation.php index d00ea2b00..270f4d55b 100644 --- a/controllers/front/failValidation.php +++ b/controllers/front/failValidation.php @@ -77,12 +77,10 @@ public function postProcess() $saferPayOrder->update(); $cartDuplicationService->restoreCart($cartId); - $isBusinessLicence = Tools::getValue(\Invertus\SaferPay\Config\SaferPayConfig::IS_BUSINESS_LICENCE); - $controller = $isBusinessLicence ? 'failIFrame' : 'fail'; $failUrl = $this->context->link->getModuleLink( $this->module->name, - $controller, + 'fail', [ 'cartId' => $cartId, 'secureKey' => $secureKey, diff --git a/controllers/front/hostedIframe.php b/controllers/front/hostedIframe.php deleted file mode 100644 index 91daa8f05..000000000 --- a/controllers/front/hostedIframe.php +++ /dev/null @@ -1,128 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ - -use Invertus\SaferPay\Config\SaferPayConfig; -use Invertus\SaferPay\Logger\LoggerInterface; -use PrestaShop\PrestaShop\Core\Checkout\TermsAndConditions; - -if (!defined('_PS_VERSION_')) { - exit; -} - -class SaferPayOfficialHostedIframeModuleFrontController extends ModuleFrontController -{ - const FILE_NAME = 'hostedIframe'; - - /** @var \SaferPayOfficial */ - public $module; - - public function initContent() - { - /** @var LoggerInterface $logger */ - $logger = $this->module->getService(LoggerInterface::class); - - $logger->debug(sprintf('%s - Controller called', self::FILE_NAME)); - - parent::initContent(); - - $paymentMethod = Tools::getValue('saved_card_method'); - $selectedCard = Tools::getValue("selectedCreditCard_{$paymentMethod}"); - - $this->context->smarty->assign([ - 'credit_card_front_url' => "{$this->module->getPathUri()}views/img/example-card/credit-card-front.png", - 'credit_card_back_url' => "{$this->module->getPathUri()}views/img/example-card/credit-card-back.png", - 'tos_cms' => SaferPayConfig::isVersionAbove177() ? $this->getDefaultTermsAndConditions() : null, - 'saferpay_selected_card' => $selectedCard, - ]); - - $logger->debug(sprintf('%s - Controller action ended', self::FILE_NAME)); - - $this->setTemplate( - SaferPayConfig::SAFERPAY_HOSTED_TEMPLATE_LOCATION . - 'template' . - Configuration::get(SaferPayConfig::HOSTED_FIELDS_TEMPLATE) . - '.tpl' - ); - } - - public function setMedia() - { - parent::setMedia(); - - Media::addJsDef([ - 'saferpay_field_access_token' => SaferPayConfig::getFieldAccessToken(), - 'saferpay_field_url' => SaferPayConfig::getFieldUrl(), - 'holder_name' => $this->module->l('Holder name', self::FILE_NAME), - 'saferpay_official_ajax_url' => $this->context->link->getModuleLink('saferpayofficial', 'ajax'), - 'saved_card_method' => Tools::getValue('saved_card_method'), - 'isBusinessLicence' => Tools::getValue('isBusinessLicence'), - ]); - - $this->context->controller->registerJavascript( - 'remote-saferpay-fields-js-lib', - SaferPayConfig::FIELDS_LIBRARY_DEFAULT_VALUE, - ['server' => 'remote', 'position' => 'bottom', 'priority' => 20] - ); - - $this->context->controller->registerJavascript( - 'hosted-template-js-init', - 'modules/' . $this->module->name . '/views/js/front/hosted-templates/template' . - Configuration::get(SaferPayConfig::HOSTED_FIELDS_TEMPLATE) . - ".js" - ); - $this->context->controller->registerJavascript( - 'hosted-template-js-submit', - 'modules/' . $this->module->name . '/views/js/front/hosted-templates/template_submit.js' - ); - - $this->context->controller->registerStylesheet( - 'theme-css', - 'modules/' . $this->module->name . '/views/css/front/hosted-templates/template' . - Configuration::get(SaferPayConfig::HOSTED_FIELDS_TEMPLATE) . - ".css" - ); - - return true; - } - - protected function getDefaultTermsAndConditions() - { - $cms = new CMS((int) Configuration::get('PS_CONDITIONS_CMS_ID'), $this->context->language->id); - - if (!Validate::isLoadedObject($cms)) { - return false; - } - - $link = $this->context->link->getCMSLink($cms, $cms->link_rewrite, (bool) Configuration::get('PS_SSL_ENABLED')); - - $termsAndConditions = new TermsAndConditions(); - $termsAndConditions - ->setText( - '[' . $cms->meta_title . ']', - $link - ) - ->setIdentifier('terms-and-conditions-footer'); - - return $termsAndConditions->format(); - } -} diff --git a/controllers/front/iframe.php b/controllers/front/iframe.php deleted file mode 100644 index 9d32ec99b..000000000 --- a/controllers/front/iframe.php +++ /dev/null @@ -1,149 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ - -use Invertus\SaferPay\Config\SaferPayConfig; -use Invertus\SaferPay\Controller\AbstractSaferPayController; -use Invertus\SaferPay\Controller\Front\CheckoutController; -use Invertus\SaferPay\Core\Payment\DTO\CheckoutData; -use Invertus\SaferPay\Enum\ControllerName; -use Invertus\SaferPay\Logger\LoggerInterface; - -if (!defined('_PS_VERSION_')) { - exit; -} - -class SaferPayOfficialIFrameModuleFrontController extends AbstractSaferPayController -{ - const FILE_NAME = 'iframe'; - - public $display_column_left = false; - - public function postProcess() - { - /** @var LoggerInterface $logger */ - $logger = $this->module->getService(LoggerInterface::class); - - $logger->debug(sprintf('%s - Controller called', self::FILE_NAME)); - - $cart = $this->context->cart; - $redirectLink = $this->context->link->getPageLink( - 'order', - true, - null, - [ - 'step' => 1, - ] - ); - if ($cart->id_customer == 0 - || $cart->id_address_delivery == 0 - || $cart->id_address_invoice == 0 - || !$this->module->active - ) { - Tools::redirect($redirectLink); - } - - $authorized = false; - foreach (Module::getPaymentModules() as $module) { - if ($module['name'] === $this->module->name) { - $authorized = true; - break; - } - } - if (!$authorized) { - $this->errors[] = $this->module->l('This payment method is not available.', self::FILE_NAME); - - $this->redirectWithNotifications($redirectLink); - } - - $customer = new Customer($cart->id_customer); - - if (!Validate::isLoadedObject($customer)) { - $logger->error(sprintf('%s - Customer not found', self::FILE_NAME), [ - 'context' => [], - 'exceptions' => [], - ]); - - Tools::redirect($redirectLink); - } - - $logger->debug(sprintf('%s - Controller action ended', self::FILE_NAME)); - } - - public function initContent() - { - parent::initContent(); - - $paymentMethod = Tools::getValue('saved_card_method'); - $selectedCard = Tools::getValue("selectedCreditCard_{$paymentMethod}"); - - try { - /** @var CheckoutController $checkoutController */ - $checkoutController = $this->module->getService(CheckoutController::class); - - // refactor it to create checkout data from validator request - $checkoutData = CheckoutData::create( - (int) $this->context->cart->id, - $paymentMethod, - (int) Tools::getValue(SaferPayConfig::IS_BUSINESS_LICENCE), - $selectedCard, - null, - null, - false, - 0 - ); - - $redirectUrl = $checkoutController->execute($checkoutData); - } catch (\Exception $exception) { - $redirectUrl = $this->context->link->getModuleLink( - $this->module->name, - ControllerName::FAIL, - [ - 'cartId' => $this->context->cart->id, - 'orderId' => Order::getIdByCartId($this->context->cart->id), - 'secureKey' => $this->context->cart->secure_key, - 'moduleId' => $this->module->id, - ], - true - ); - $this->redirectWithNotifications($redirectUrl); - } - - $this->context->smarty->assign([ - 'redirect' => $redirectUrl, - ]); - - $this->setTemplate(SaferPayConfig::SAFERPAY_TEMPLATE_LOCATION . '/front/saferpay_iframe.tpl'); - } - - public function setMedia() - { - parent::setMedia(); - - $this->registerStylesheet( - $this->module->name . '-iframe', - 'modules/' . $this->module->name . '/views/css/front/saferpay_iframe.css' - ); - - return true; - } -} diff --git a/controllers/front/notify.php b/controllers/front/notify.php index 85938721c..e1fc6bbf1 100644 --- a/controllers/front/notify.php +++ b/controllers/front/notify.php @@ -140,27 +140,50 @@ public function postProcess() // Must be left below assert action to get newest information. $order = new Order($orderId); + $paymentBehaviorWithout3D = (int) Configuration::get(SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D); + if (!$assertResponseBody->getLiability()->getLiabilityShift() && - in_array($order->payment, SaferPayConfig::SUPPORTED_3DS_PAYMENT_METHODS) && - (int) Configuration::get(SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D) === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CANCEL + in_array($order->payment, SaferPayConfig::SUPPORTED_3DS_PAYMENT_METHODS) ) { /** @var SaferPayOrderStatusService $orderStatusService */ $orderStatusService = $this->module->getService(SaferPayOrderStatusService::class); - $orderStatusService->cancel($order); - $logger->debug(sprintf('%s - Liability shift is false', self::FILE_NAME), [ - 'context' => [ - 'id_order' => $order->id, - ], - ]); + if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CANCEL) { + $orderStatusService->cancel($order); - $logger->debug(sprintf('%s - liability shift is false', self::FILE_NAME), [ - 'context' => [ - 'id_order' => $order->id, - ], - ]); + $logger->debug(sprintf('%s - Liability shift is false, canceling order', self::FILE_NAME), [ + 'context' => [ + 'id_order' => $order->id, + ], + ]); - die($this->module->l('Liability shift is false', self::FILE_NAME)); + die($this->module->l('Liability shift is false', self::FILE_NAME)); + } + + if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_AUTHORIZE) { + $logger->debug(sprintf('%s - Liability shift is false, order left authorized', self::FILE_NAME), [ + 'context' => [ + 'id_order' => $order->id, + ], + ]); + + die($this->module->l('Liability shift is false, order left authorized', self::FILE_NAME)); + } + + if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CAPTURE + && SaferPayConfig::supportsOrderCapture($order->payment) + && $transactionStatus !== TransactionStatus::CAPTURED + ) { + $orderStatusService->capture($order); + + $logger->debug(sprintf('%s - Liability shift is false, capturing order', self::FILE_NAME), [ + 'context' => [ + 'id_order' => $order->id, + ], + ]); + + die($this->module->l('Liability shift is false, capturing order', self::FILE_NAME)); + } } //NOTE to get latest information possible and not override new information. diff --git a/controllers/front/return.php b/controllers/front/return.php index b5fc37c69..366fd7eec 100644 --- a/controllers/front/return.php +++ b/controllers/front/return.php @@ -112,8 +112,7 @@ public function postProcess() /** @var PaymentTypeProvider $paymentTypeProvider */ $paymentTypeProvider = $this->module->getService(PaymentTypeProvider::class); - if ($paymentTypeProvider->get($orderPayment) === PaymentType::IFRAME - || $paymentTypeProvider->get($orderPayment) === PaymentType::HOSTED_IFRAME) { + if ($paymentTypeProvider->get($orderPayment) === PaymentType::HOSTED_IFRAME) { $order = new Order(Order::getIdByCartId($cartId)); try { @@ -235,17 +234,11 @@ public function initContent() private function getSuccessControllerName($isBusinessLicence, $fieldToken, $usingSavedCard) { - $successController = ControllerName::SUCCESS; - - if ($isBusinessLicence) { - $successController = ControllerName::SUCCESS_IFRAME; - } - if ($fieldToken || $usingSavedCard) { - $successController = ControllerName::SUCCESS_HOSTED; + return ControllerName::SUCCESS_HOSTED; } - return $successController; + return ControllerName::SUCCESS; } /** @@ -274,7 +267,9 @@ private function executeTransaction($orderId, $selectedCard) */ private function getRedirectionToControllerUrl($controllerName) { - $cartId = $this->context->cart->id ? $this->context->cart->id : Tools::getValue('cartId'); + $cartId = (int) Tools::getValue('cartId') ?: (int) $this->context->cart->id; + $cart = new Cart($cartId); + $secureKey = Validate::isLoadedObject($cart) ? $cart->secure_key : $this->context->cart->secure_key; return $this->context->link->getModuleLink( $this->module->name, @@ -282,7 +277,7 @@ private function getRedirectionToControllerUrl($controllerName) [ 'cartId' => $cartId, 'orderId' => Order::getIdByCartId($cartId), - 'secureKey' => $this->context->cart->secure_key, + 'secureKey' => $secureKey, 'moduleId' => $this->module->id, ] ); @@ -329,19 +324,39 @@ private function createAndValidateOrder($assertResponseBody, $transactionStatus, $orderId = Order::getIdByCartId($cartId); $order = new Order($orderId); + $paymentBehaviorWithout3D = (int) Configuration::get(SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D); + if (!$assertResponseBody->getLiability()->getLiabilityShift() && - in_array($order->payment, SaferPayConfig::SUPPORTED_3DS_PAYMENT_METHODS) && - (int) Configuration::get(SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D) === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CANCEL + in_array($order->payment, SaferPayConfig::SUPPORTED_3DS_PAYMENT_METHODS) ) { /** @var SaferPayOrderStatusService $orderStatusService */ $orderStatusService = $this->module->getService(SaferPayOrderStatusService::class); - $orderStatusService->cancel($order); + + if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CANCEL) { + $orderStatusService->cancel($order); + + return; + } + + if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_AUTHORIZE) { + return; + } + + if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CAPTURE + && SaferPayConfig::supportsOrderCapture($order->payment) + && $transactionStatus !== TransactionStatus::CAPTURED + ) { + $orderStatusService->capture($order); + + return; + } } //NOTE to get latest information possible and not override new information. - $paymentMethod = $assertResponseBody->getPaymentMeans()->getBrand()->getPaymentMethod();// if payment does not support order capture, it means it always auto-captures it (at least with accountToAccount payment), + $paymentMethod = $assertResponseBody->getPaymentMeans()->getBrand()->getPaymentMethod(); + // if payment does not support order capture, it means it always auto-captures it (at least with accountToAccount payment), // so in this case if status comes back "captured" we just update the order state accordingly if (!SaferPayConfig::supportsOrderCapture($paymentMethod) && $transactionStatus === TransactionStatus::CAPTURED @@ -367,30 +382,9 @@ private function createAndValidateOrder($assertResponseBody, $transactionStatus, private function getFailController($orderPayment) { - /** @var PaymentTypeProvider $paymentTypeProvider */ - $paymentTypeProvider = $this->module->getService(PaymentTypeProvider::class); - /** @var LoggerInterface $logger */ $logger = $this->module->getService(LoggerInterface::class); - $logger->debug('Getting fail controller', [ - 'context' => [], - 'controller' => self::FILE_NAME, - 'order_payment' => $orderPayment, - ]); - - $paymentRedirectType = $paymentTypeProvider->get($orderPayment); - - if ($paymentRedirectType === PaymentType::IFRAME) { - $logger->debug('Fail controller is FAIL_IFRAME', [ - 'context' => [], - 'controller' => self::FILE_NAME, - 'order_payment' => $orderPayment, - ]); - - return ControllerName::FAIL_IFRAME; - } - $logger->debug('Fail controller is FAIL', [ 'context' => [], 'controller' => self::FILE_NAME, diff --git a/controllers/front/successIFrame.php b/controllers/front/successIFrame.php deleted file mode 100644 index a75980d9e..000000000 --- a/controllers/front/successIFrame.php +++ /dev/null @@ -1,203 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ - -use Invertus\SaferPay\Config\SaferPayConfig; -use Invertus\SaferPay\Controller\AbstractSaferPayController; -use Invertus\SaferPay\Enum\ControllerName; -use Invertus\SaferPay\Logger\LoggerInterface; -use Invertus\SaferPay\Utility\ExceptionUtility; - -if (!defined('_PS_VERSION_')) { - exit; -} - -class SaferPayOfficialSuccessIFrameModuleFrontController extends AbstractSaferPayController -{ - const FILE_NAME = 'successIFrame'; - - protected $display_header = false; - protected $display_footer = false; - - public function init() - { - $this->display_header = true; - - parent::init(); - } - - public function postProcess() // todo refactor this by the logic provided - { - /** @var LoggerInterface $logger */ - $logger = $this->module->getService(LoggerInterface::class); - - $logger->debug(sprintf('%s - Controller called', self::FILE_NAME)); - - $cartId = Tools::getValue('cartId'); - $orderId = Tools::getValue('orderId'); - $secureKey = Tools::getValue('secureKey'); - $moduleId = Tools::getValue('moduleId'); - - $cart = new Cart($cartId); - - if ($cart->secure_key !== $secureKey) { - $this->errors[] = $this->module->l('Failed to validate cart.', self::FILE_NAME); - - $this->redirectWithNotifications($this->getOrderLink()); - } - - /** Purchase is made with card that needs to be saved */ - if (Tools::getValue('selectedCard') <= 0) { - return; - } - - try { - $logger->debug(sprintf('%s - Controller action ended', self::FILE_NAME)); - - Tools::redirect($this->getOrderConfirmationLink($cartId, $moduleId, $orderId, $secureKey)); - } catch (Exception $e) { - $logger->error($e->getMessage(), [ - 'context' => [], - 'exceptions' => ExceptionUtility::getExceptions($e), - ]); - - Tools::redirect( - $this->context->link->getModuleLink( - $this->module->name, - ControllerName::FAIL_IFRAME, - [ - 'cartId' => $cartId, - 'secureKey' => $secureKey, - 'orderId' => $orderId, - \Invertus\SaferPay\Config\SaferPayConfig::IS_BUSINESS_LICENCE => true, - ], - true - ) - ); - } - } - - public function initContent() - { - parent::initContent(); - $cartId = Tools::getValue('cartId'); - $moduleId = Tools::getValue('moduleId'); - $orderId = Tools::getValue('orderId'); - $secureKey = Tools::getValue('secureKey'); - - $orderLink = $this->context->link->getPageLink( - 'order-confirmation', - true, - null, - [ - 'id_cart' => $cartId, - 'id_module' => $moduleId, - 'id_order' => $orderId, - 'key' => $secureKey, - ] - ); - - $this->registerStylesheet( - $this->module->name . '-loading', - 'modules/' . $this->module->name . '/views/css/front/loading.css' - ); - - Media::addJsDef([ - 'redirectUrl' => $orderLink, - ]); - - $this->setTemplate(SaferPayConfig::SAFERPAY_TEMPLATE_LOCATION . '/front/loading.tpl'); - } - - public function setMedia() - { - parent::setMedia(); - - $cartId = Tools::getValue('cartId'); - $moduleId = Tools::getValue('moduleId'); - $orderId = Tools::getValue('orderId'); - $secureKey = Tools::getValue('secureKey'); - - $orderLink = $this->context->link->getPageLink( - 'order-confirmation', - true, - null, - [ - 'id_cart' => $cartId, - 'id_module' => $moduleId, - 'id_order' => $orderId, - 'key' => $secureKey, - ] - ); - - $this->registerStylesheet( - $this->module->name . '-loading', - 'modules/' . $this->module->name . '/views/css/front/loading.css' - ); - - Media::addJsDef([ - 'redirectUrl' => $orderLink, - ]); - - $this->context->controller->registerJavascript( - $this->module->name . '-iframe', - 'modules/' . $this->module->name . '/views/js/front/saferpay_iframe.js' - ); - - return true; - } - - /** - * @param int $cartId - * @param int $moduleId - * @param int $orderId - * @param string $secureKey - * - * @return string - */ - private function getOrderConfirmationLink($cartId, $moduleId, $orderId, $secureKey) - { - return $this->context->link->getPageLink( - 'order-confirmation', - true, - null, - [ - 'id_cart' => $cartId, - 'id_module' => $moduleId, - 'id_order' => $orderId, - 'key' => $secureKey, - ] - ); - } - - private function getOrderLink() - { - return $this->context->link->getPageLink( - 'order', - true, - null, - [ - 'step' => 1, - ] - ); - } -} diff --git a/cypress/integration/01_ps1764.Module.Configure.cy.js b/cypress/integration/01_ps1764.Module.Configure.cy.js index b7313cb4c..f8988d2b8 100644 --- a/cypress/integration/01_ps1764.Module.Configure.cy.js +++ b/cypress/integration/01_ps1764.Module.Configure.cy.js @@ -117,14 +117,6 @@ it('04 Fields and Logs tabs are shown OK', () => { cy.get('.pstaggerAddTagInput').type('saferpay') cy.get('#module-search-button').click() cy.get('.btn-group > .btn-primary-reverse').click() //clicking the Congifure - cy.get('#subtab-AdminSaferPayOfficialFields').click() - cy.get('[id="configuration_form"]').should('be.visible') - cy.get('.field-container > :nth-child(1) > img').click() - cy.get(':nth-child(2) > img').click() - cy.get(':nth-child(3) > img').click() - cy.get('[class="alert alert-info"]').should('be.visible') - cy.get('[name="submitOptionsconfiguration"]').click() - cy.get('[class="alert alert-success"]').should('be.visible') cy.get('#subtab-AdminSaferPayOfficialLogs').click() cy.get('[id="form-saferpay_log"]').should('be.visible') }) diff --git a/cypress/integration/01_ps1770.Module.Configure.cy.js b/cypress/integration/01_ps1770.Module.Configure.cy.js index 9696c496c..1de0a18c6 100644 --- a/cypress/integration/01_ps1770.Module.Configure.cy.js +++ b/cypress/integration/01_ps1770.Module.Configure.cy.js @@ -117,14 +117,6 @@ it('04 Fields and Logs tabs are shown OK', () => { cy.get('.pstaggerAddTagInput').type('saferpay') cy.get('#module-search-button').click() cy.get('.btn-group > .btn-primary-reverse').click() //clicking the Congifure - cy.get('#subtab-AdminSaferPayOfficialFields').click() - cy.get('[id="configuration_form"]').should('be.visible') - cy.get('.field-container > :nth-child(1) > img').click() - cy.get(':nth-child(2) > img').click() - cy.get(':nth-child(3) > img').click() - cy.get('[class="alert alert-info"]').should('be.visible') - cy.get('[name="submitOptionsconfiguration"]').click() - cy.get('[class="alert alert-success"]').should('be.visible') cy.get('#subtab-AdminSaferPayOfficialLogs').click() cy.get('[id="form-saferpay_log"]').should('be.visible') }) diff --git a/cypress/integration/01_ps1784.Module.Configure.cy.js b/cypress/integration/01_ps1784.Module.Configure.cy.js index 8da2aae5b..716aa4837 100644 --- a/cypress/integration/01_ps1784.Module.Configure.cy.js +++ b/cypress/integration/01_ps1784.Module.Configure.cy.js @@ -117,14 +117,6 @@ it('04 Fields and Logs tabs are shown OK', () => { cy.get('.pstaggerAddTagInput').type('saferpay') cy.get('#module-search-button').click() cy.get('.btn-group > .btn-primary-reverse').click() //clicking the Congifure - cy.get('#subtab-AdminSaferPayOfficialFields').click() - cy.get('[id="configuration_form"]').should('be.visible') - cy.get('.field-container > :nth-child(1) > img').click() - cy.get(':nth-child(2) > img').click() - cy.get(':nth-child(3) > img').click() - cy.get('[class="alert alert-info"]').should('be.visible') - cy.get('[name="submitOptionsconfiguration"]').click() - cy.get('[class="alert alert-success"]').should('be.visible') cy.get('#subtab-AdminSaferPayOfficialLogs').click() cy.get('[id="form-saferpay_log"]').should('be.visible') }) diff --git a/cypress/integration/01_ps1786.Module.Configure.cy.js b/cypress/integration/01_ps1786.Module.Configure.cy.js index 0c8259a9d..863cb240b 100644 --- a/cypress/integration/01_ps1786.Module.Configure.cy.js +++ b/cypress/integration/01_ps1786.Module.Configure.cy.js @@ -117,14 +117,6 @@ it('04 Fields and Logs tabs are shown OK', () => { cy.get('.pstaggerAddTagInput').type('saferpay') cy.get('#module-search-button').click() cy.get('.btn-group > .btn-primary-reverse').click() //clicking the Congifure - cy.get('#subtab-AdminSaferPayOfficialFields').click() - cy.get('[id="configuration_form"]').should('be.visible') - cy.get('.field-container > :nth-child(1) > img').click() - cy.get(':nth-child(2) > img').click() - cy.get(':nth-child(3) > img').click() - cy.get('[class="alert alert-info"]').should('be.visible') - cy.get('[name="submitOptionsconfiguration"]').click() - cy.get('[class="alert alert-success"]').should('be.visible') cy.get('#subtab-AdminSaferPayOfficialLogs').click() cy.get('[id="form-saferpay_log"]').should('be.visible') }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 000000000..cb24cfd8f --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1367 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + cypress: + specifier: ^9.6.1 + version: 9.7.0 + cypress-iframe: + specifier: ^1.0.1 + version: 1.0.1(@types/cypress@1.1.6) + +packages: + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@cypress/request@2.88.12': + resolution: {integrity: sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==} + engines: {node: '>= 6'} + + '@cypress/xvfb@1.2.4': + resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} + + '@types/cypress@1.1.6': + resolution: {integrity: sha512-CfeLLD3+6vIWe2AO5hR63f1c8EbRzrp/j1ExubAwOTpwZFZvF3Nm9cOPQiUwzNmAUmZuhO0QVH98Qlujni6nPw==} + deprecated: This is a stub types definition. cypress provides its own type definitions, so you do not need this installed. + + '@types/node@14.18.63': + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + + '@types/sinonjs__fake-timers@8.1.1': + resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} + + '@types/sizzle@2.3.10': + resolution: {integrity: sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + blob-util@2.0.2: + resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + cachedir@2.4.0: + resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} + engines: {node: '>=6'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-more-types@2.24.0: + resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} + engines: {node: '>= 0.8.0'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cypress-iframe@1.0.1: + resolution: {integrity: sha512-Ne+xkZmWMhfq3x6wbfzK/SzsVTCrJru3R3cLXsoSAZyfUtJDamXyaIieHXeea3pQDXF4wE2w4iUuvCYHhoD31g==} + peerDependencies: + '@types/cypress': ^1.1.0 + + cypress@9.7.0: + resolution: {integrity: sha512-+1EE1nuuuwIt/N1KXRR2iWHU+OiIt7H28jJDyyI4tiUftId/DrXYEwoDa5+kH2pki1zxnA0r6HrUGHV5eLbF5Q==} + engines: {node: '>=12.0.0'} + hasBin: true + + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + + execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + + executable@4.1.1: + resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} + engines: {node: '>=4'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + getos@3.2.1: + resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} + + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-signature@1.3.6: + resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==} + engines: {node: '>=0.10'} + + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsprim@2.0.2: + resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} + engines: {'0': node >=0.6.0} + + lazy-ass@1.6.0: + resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} + engines: {node: '> 0.8'} + + listr2@3.14.0: + resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==} + engines: {node: '>=10.0.0'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + ospath@1.2.2: + resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + proxy-from-env@1.0.0: + resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.10.4: + resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==} + engines: {node: '>=0.6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + request-progress@3.0.0: + resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + throttleit@1.0.1: + resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + +snapshots: + + '@colors/colors@1.5.0': + optional: true + + '@cypress/request@2.88.12': + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + http-signature: 1.3.6 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + performance-now: 2.1.0 + qs: 6.10.4 + safe-buffer: 5.2.1 + tough-cookie: 4.1.4 + tunnel-agent: 0.6.0 + uuid: 8.3.2 + + '@cypress/xvfb@1.2.4(supports-color@8.1.1)': + dependencies: + debug: 3.2.7(supports-color@8.1.1) + lodash.once: 4.1.1 + transitivePeerDependencies: + - supports-color + + '@types/cypress@1.1.6': + dependencies: + cypress: 9.7.0 + + '@types/node@14.18.63': {} + + '@types/sinonjs__fake-timers@8.1.1': {} + + '@types/sizzle@2.3.10': {} + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 14.18.63 + optional: true + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + arch@2.2.0: {} + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assert-plus@1.0.0: {} + + astral-regex@2.0.0: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + aws-sign2@0.7.0: {} + + aws4@1.13.2: {} + + base64-js@1.5.1: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + + blob-util@2.0.2: {} + + bluebird@3.7.2: {} + + buffer-crc32@0.2.13: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + cachedir@2.4.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caseless@0.12.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-more-types@2.24.0: {} + + ci-info@3.9.0: {} + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@5.1.0: {} + + common-tags@1.8.2: {} + + core-util-is@1.0.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cypress-iframe@1.0.1(@types/cypress@1.1.6): + dependencies: + '@types/cypress': 1.1.6 + + cypress@9.7.0: + dependencies: + '@cypress/request': 2.88.12 + '@cypress/xvfb': 1.2.4(supports-color@8.1.1) + '@types/node': 14.18.63 + '@types/sinonjs__fake-timers': 8.1.1 + '@types/sizzle': 2.3.10 + arch: 2.2.0 + blob-util: 2.0.2 + bluebird: 3.7.2 + buffer: 5.7.1 + cachedir: 2.4.0 + chalk: 4.1.2 + check-more-types: 2.24.0 + cli-cursor: 3.1.0 + cli-table3: 0.6.5 + commander: 5.1.0 + common-tags: 1.8.2 + dayjs: 1.11.19 + debug: 4.4.3(supports-color@8.1.1) + enquirer: 2.4.1 + eventemitter2: 6.4.9 + execa: 4.1.0 + executable: 4.1.1 + extract-zip: 2.0.1(supports-color@8.1.1) + figures: 3.2.0 + fs-extra: 9.1.0 + getos: 3.2.1 + is-ci: 3.0.1 + is-installed-globally: 0.4.0 + lazy-ass: 1.6.0 + listr2: 3.14.0(enquirer@2.4.1) + lodash: 4.17.23 + log-symbols: 4.1.0 + minimist: 1.2.8 + ospath: 1.2.2 + pretty-bytes: 5.6.0 + proxy-from-env: 1.0.0 + request-progress: 3.0.0 + semver: 7.7.4 + supports-color: 8.1.1 + tmp: 0.2.5 + untildify: 4.0.0 + yauzl: 2.10.0 + + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + + dayjs@1.11.19: {} + + debug@3.2.7(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + escape-string-regexp@1.0.5: {} + + eventemitter2@6.4.9: {} + + execa@4.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + executable@4.1.1: + dependencies: + pify: 2.3.0 + + extend@3.0.2: {} + + extract-zip@2.0.1(supports-color@8.1.1): + dependencies: + debug: 4.4.3(supports-color@8.1.1) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + extsprintf@1.3.0: {} + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + forever-agent@0.6.1: {} + + form-data@2.3.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + + getos@3.2.1: + dependencies: + async: 3.2.6 + + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-signature@1.3.6: + dependencies: + assert-plus: 1.0.0 + jsprim: 2.0.2 + sshpk: 1.18.0 + + human-signals@1.1.1: {} + + ieee754@1.2.1: {} + + indent-string@4.0.0: {} + + ini@2.0.0: {} + + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + + is-fullwidth-code-point@3.0.0: {} + + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + + is-path-inside@3.0.3: {} + + is-stream@2.0.1: {} + + is-typedarray@1.0.0: {} + + is-unicode-supported@0.1.0: {} + + isexe@2.0.0: {} + + isstream@0.1.2: {} + + jsbn@0.1.1: {} + + json-schema@0.4.0: {} + + json-stringify-safe@5.0.1: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsprim@2.0.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + + lazy-ass@1.6.0: {} + + listr2@3.14.0(enquirer@2.4.1): + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.4.1 + rxjs: 7.8.2 + through: 2.3.8 + wrap-ansi: 7.0.0 + optionalDependencies: + enquirer: 2.4.1 + + lodash.once@4.1.1: {} + + lodash@4.17.23: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@4.0.0: + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + + math-intrinsics@1.1.0: {} + + merge-stream@2.0.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + minimist@1.2.8: {} + + ms@2.1.3: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + object-inspect@1.13.4: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + ospath@1.2.2: {} + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + path-key@3.1.1: {} + + pend@1.2.0: {} + + performance-now@2.1.0: {} + + pify@2.3.0: {} + + pretty-bytes@5.6.0: {} + + proxy-from-env@1.0.0: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + qs@6.10.4: + dependencies: + side-channel: 1.1.0 + + querystringify@2.2.0: {} + + request-progress@3.0.0: + dependencies: + throttleit: 1.0.1 + + requires-port@1.0.0: {} + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + rfdc@1.4.1: {} + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-final-newline@2.0.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + throttleit@1.0.1: {} + + through@2.3.8: {} + + tmp@0.2.5: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tweetnacl@0.14.5: {} + + type-fest@0.21.3: {} + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + untildify@4.0.0: {} + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + uuid@8.3.2: {} + + verror@1.10.0: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 diff --git a/saferpayofficial.php b/saferpayofficial.php index da83d0cf4..e5e447171 100644 --- a/saferpayofficial.php +++ b/saferpayofficial.php @@ -55,7 +55,6 @@ class SaferPayOfficial extends PaymentModule const ADMIN_SAFERPAY_MODULE_CONTROLLER = 'AdminSaferPayOfficialModule'; const ADMIN_SETTINGS_CONTROLLER = 'AdminSaferPayOfficialSettings'; const ADMIN_PAYMENTS_CONTROLLER = 'AdminSaferPayOfficialPayment'; - const ADMIN_FIELDS_CONTROLLER = 'AdminSaferPayOfficialFields'; const ADMIN_ORDER_CONTROLLER = 'AdminSaferPayOfficialOrder'; const ADMIN_LOGS_CONTROLLER = 'AdminSaferPayOfficialLogs'; @@ -68,7 +67,7 @@ public function __construct($name = null) { $this->name = 'saferpayofficial'; $this->author = 'Invertus'; - $this->version = '2.0.3'; + $this->version = '2.1.0'; $this->module_key = '3d3506c3e184a1fe63b936b82bda1bdf'; $this->displayName = 'SaferpayOfficial'; $this->description = 'Saferpay Payment module'; diff --git a/src/Api/ApiRequest.php b/src/Api/ApiRequest.php index 5f02cba3d..073056e91 100644 --- a/src/Api/ApiRequest.php +++ b/src/Api/ApiRequest.php @@ -114,15 +114,131 @@ public function get(string $url, array $params = []): ?\stdClass return json_decode($response->raw_body); } catch (Exception $exception) { - $this->logger->error($exception->getMessage(), [ + if ($response === null) { + $this->logger->error($exception->getMessage(), [ + 'context' => [ + 'headers' => $this->getHeaders(), + ], + 'request' => $params, + 'response' => null, + 'exceptions' => ExceptionUtility::getExceptions($exception), + ]); + } + + throw $exception; + } + } + + /** + * API Request Get Method with explicit credentials. + * + * @param string $url + * @param string $username + * @param string $password + * @param string $baseUrl + * @param array $params + * @return mixed + * @throws Exception + */ + public function getWithCredentials($url, $username, $password, $baseUrl, $params = []) + { + $response = null; + + try { + $credentials = base64_encode("$username:$password"); + $headers = [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Saferpay-ApiVersion' => SaferPayConfig::API_VERSION, + 'Saferpay-RequestId' => 'false', + 'Authorization' => "Basic $credentials", + ]; + + $response = Request::get( + $baseUrl . $url, + $headers, + $params + ); + + $this->logger->debug(sprintf('%s - GET (credentials) response: %d', self::FILE_NAME, $response->code), [ 'context' => [ - 'headers' => $this->getHeaders(), + 'uri' => $baseUrl . $url, + ], + 'request' => $params, + 'response' => $response->body, + ]); + + $this->isValidResponse($response); + + return json_decode($response->raw_body); + } catch (Exception $exception) { + if ($response === null) { + $this->logger->error($exception->getMessage(), [ + 'context' => [], + 'request' => $params, + 'response' => null, + 'exceptions' => ExceptionUtility::getExceptions($exception), + ]); + } + + throw $exception; + } + } + + /** + * API Request Post Method with explicit credentials. + * + * @param string $url + * @param string $username + * @param string $password + * @param string $baseUrl + * @param array|null $params + * @return mixed + * @throws Exception + */ + public function postWithCredentials($url, $username, $password, $baseUrl, $params = null) + { + $response = null; + + try { + $credentials = base64_encode("$username:$password"); + $headers = [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Saferpay-ApiVersion' => SaferPayConfig::API_VERSION, + 'Saferpay-RequestId' => 'false', + 'Authorization' => "Basic $credentials", + ]; + + $body = $params !== null ? json_encode($params) : '{}'; + + $response = Request::post( + $baseUrl . $url, + $headers, + $body + ); + + $this->logger->debug(sprintf('%s - POST (credentials) response: %d', self::FILE_NAME, $response->code), [ + 'context' => [ + 'uri' => $baseUrl . $url, ], 'request' => $params, - 'response' => json_decode($response->raw_body), - 'exceptions' => ExceptionUtility::getExceptions($exception), + 'response' => $response->body, ]); + $this->isValidResponse($response); + + return json_decode($response->raw_body); + } catch (Exception $exception) { + if ($response === null) { + $this->logger->error($exception->getMessage(), [ + 'context' => [], + 'request' => $params, + 'response' => null, + 'exceptions' => ExceptionUtility::getExceptions($exception), + ]); + } + throw $exception; } } diff --git a/src/Api/Request/GenerateFieldAccessTokenService.php b/src/Api/Request/GenerateFieldAccessTokenService.php new file mode 100644 index 000000000..3ef46f9a9 --- /dev/null +++ b/src/Api/Request/GenerateFieldAccessTokenService.php @@ -0,0 +1,61 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Api\Request; + +use Invertus\SaferPay\Api\ApiRequest; +use Invertus\SaferPay\DTO\Request\GenerateFieldAccessToken\GenerateFieldAccessTokenRequest; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class GenerateFieldAccessTokenService +{ + /** @var ApiRequest */ + private $apiRequest; + + public function __construct(ApiRequest $apiRequest) + { + $this->apiRequest = $apiRequest; + } + + /** + * @param GenerateFieldAccessTokenRequest $request + * @param string $username + * @param string $password + * @param string $baseUrl + * @param array|null $params + * @return mixed + */ + public function generateToken(GenerateFieldAccessTokenRequest $request, $username, $password, $baseUrl, $params = null) + { + return $this->apiRequest->postWithCredentials( + $request->generateRequestUrl(), + $username, + $password, + $baseUrl, + $params + ); + } +} diff --git a/src/Api/Request/GetLicenseService.php b/src/Api/Request/GetLicenseService.php new file mode 100644 index 000000000..0299875fb --- /dev/null +++ b/src/Api/Request/GetLicenseService.php @@ -0,0 +1,76 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Api\Request; + +use Invertus\SaferPay\Api\ApiRequest; +use Invertus\SaferPay\DTO\Request\GetLicense\GetLicenseRequest; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class GetLicenseService +{ + /** @var ApiRequest */ + private $apiRequest; + + public function __construct(ApiRequest $apiRequest) + { + $this->apiRequest = $apiRequest; + } + + /** + * @param GetLicenseRequest $request + * @param string $username + * @param string $password + * @param string $baseUrl + * @return mixed + */ + public function getLicense(GetLicenseRequest $request, $username, $password, $baseUrl) + { + return $this->apiRequest->getWithCredentials( + $request->generateRequestUrl(), + $username, + $password, + $baseUrl + ); + } + + /** + * @param GetLicenseRequest $request + * @param string $username + * @param string $password + * @param string $baseUrl + * @return mixed + */ + public function getLicenseFallback(GetLicenseRequest $request, $username, $password, $baseUrl) + { + return $this->apiRequest->getWithCredentials( + $request->generateFallbackRequestUrl(), + $username, + $password, + $baseUrl + ); + } +} diff --git a/views/css/front/hosted-templates/index.php b/src/Api/Request/GetTerminalsService.php similarity index 50% rename from views/css/front/hosted-templates/index.php rename to src/Api/Request/GetTerminalsService.php index ee6227264..6ca24b6b3 100644 --- a/views/css/front/hosted-templates/index.php +++ b/src/Api/Request/GetTerminalsService.php @@ -20,12 +20,40 @@ *@copyright SIX Payment Services *@license SIX Payment Services */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); +namespace Invertus\SaferPay\Api\Request; -header('Location: ../'); -exit; +use Invertus\SaferPay\Api\ApiRequest; +use Invertus\SaferPay\DTO\Request\GetTerminals\GetTerminalsRequest; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class GetTerminalsService +{ + /** @var ApiRequest */ + private $apiRequest; + + public function __construct(ApiRequest $apiRequest) + { + $this->apiRequest = $apiRequest; + } + + /** + * @param GetTerminalsRequest $request + * @param string $username + * @param string $password + * @param string $baseUrl + * @return mixed + */ + public function getTerminals(GetTerminalsRequest $request, $username, $password, $baseUrl) + { + return $this->apiRequest->getWithCredentials( + $request->generateRequestUrl(), + $username, + $password, + $baseUrl + ); + } +} diff --git a/src/Config/SaferPayConfig.php b/src/Config/SaferPayConfig.php index e17917fb9..9b36ad368 100644 --- a/src/Config/SaferPayConfig.php +++ b/src/Config/SaferPayConfig.php @@ -255,7 +255,6 @@ class SaferPayConfig const SAFERPAY_PAYMENT_DESCRIPTION_DEFAULT_VALUE = 'Prestashop Payment'; const SAFERPAY_TEMPLATE_LOCATION = 'module:saferpayofficial/views/templates/'; - const SAFERPAY_HOSTED_TEMPLATE_LOCATION = 'module:saferpayofficial/views/templates/front/hosted-templates/'; const AMOUNT_MULTIPLIER_FOR_API = 100; const DEFAULT_PAYMENT_BEHAVIOR_CAPTURE = 0; @@ -267,15 +266,15 @@ class SaferPayConfig const FIELDS_LIBRARY = 'SAFERPAY_FIELDS_JAVASCRIPT_LIBRARY'; const FIELDS_LIBRARY_DEFAULT_VALUE = 'https://www.saferpay.com/Fields/lib/1/saferpay-fields.js'; - const HOSTED_FIELDS_TEMPLATE_DEFAULT = 1; - const HOSTED_FIELDS_TEMPLATE = 'SAFERPAY_HOSTED_FIELDS_TEMPLATE'; - const IS_BUSINESS_LICENCE = 'isBusinessLicence'; const EMAIL_ALERTS_MODULE_NAME = 'ps_emailalerts'; const PAYMENT_BEHAVIOR_WITHOUT_3D_CANCEL = 0; const PAYMENT_BEHAVIOR_WITHOUT_3D_AUTHORIZE = 1; + const PAYMENT_BEHAVIOR_WITHOUT_3D_CAPTURE = 2; + + const SAFERPAY_ORDER_ID_OPTION = 'SAFERPAY_ORDER_ID_OPTION'; const SAFERPAY_CARDFORM_HOLDERNAME_REQUIRENCE = 'MANDATORY'; const SAFERPAY_DEBUG_MODE = 'SAFERPAY_DEBUG_MODE'; @@ -436,12 +435,12 @@ public static function getDefaultConfiguration() RequestHeader::SPEC_REFUND_VERSION => SaferPayConfig::API_VERSION, RequestHeader::RETRY_INDICATOR => 0, SaferPayConfig::PAYMENT_BEHAVIOR => 1, - SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D => 1, + SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D => 0, SaferPayConfig::SAFERPAY_ALLOW_SAFERPAY_SEND_CUSTOMER_MAIL => 1, SaferPayConfig::SAFERPAY_PAYMENT_DESCRIPTION => self::SAFERPAY_PAYMENT_DESCRIPTION_DEFAULT_VALUE, self::SAFERPAY_ORDER_CREATION_AFTER_AUTHORIZATION => 0, + self::SAFERPAY_ORDER_ID_OPTION => 0, self::TEST_MODE => 1, - self::HOSTED_FIELDS_TEMPLATE => self::HOSTED_FIELDS_TEMPLATE_DEFAULT, self::SAFERPAY_ORDER_STATE_CHOICE_AWAITING_PAYMENT => (int) Configuration::get( self::SAFERPAY_PAYMENT_AWAITING ), @@ -478,6 +477,7 @@ public static function getUninstallConfiguration() self::FIELDS_ACCESS_TOKEN, self::FIELDS_ACCESS_TOKEN . self::TEST_SUFFIX, self::SAFERPAY_ORDER_CREATION_AFTER_AUTHORIZATION, + self::SAFERPAY_ORDER_ID_OPTION, self::SAFERPAY_SEND_ORDER_CONF_MAIL, self::SAFERPAY_GROUP_CARDS, ]; diff --git a/src/DTO/Request/GenerateFieldAccessToken/GenerateFieldAccessTokenRequest.php b/src/DTO/Request/GenerateFieldAccessToken/GenerateFieldAccessTokenRequest.php new file mode 100644 index 000000000..5eeb4a490 --- /dev/null +++ b/src/DTO/Request/GenerateFieldAccessToken/GenerateFieldAccessTokenRequest.php @@ -0,0 +1,67 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\DTO\Request\GenerateFieldAccessToken; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class GenerateFieldAccessTokenRequest +{ + /** @var string */ + private $customerId; + + /** @var string */ + private $terminalId; + + /** + * @param string $customerId + * @param string $terminalId + */ + public function __construct($customerId, $terminalId) + { + if (!preg_match('/^[a-zA-Z0-9\-_]+$/', $customerId)) { + throw new \InvalidArgumentException('Invalid customer ID format'); + } + + if (!preg_match('/^[a-zA-Z0-9\-_]+$/', $terminalId)) { + throw new \InvalidArgumentException('Invalid terminal ID format'); + } + + $this->customerId = $customerId; + $this->terminalId = $terminalId; + } + + /** + * @return string + */ + public function generateRequestUrl() + { + return sprintf( + 'rest/customers/%s/terminals/%s/fields-access-tokens', + $this->customerId, + $this->terminalId + ); + } +} diff --git a/views/css/admin/saferpay_fields.css b/src/DTO/Request/GetLicense/GetLicenseRequest.php similarity index 50% rename from views/css/admin/saferpay_fields.css rename to src/DTO/Request/GetLicense/GetLicenseRequest.php index 064c2c333..0cf139810 100644 --- a/views/css/admin/saferpay_fields.css +++ b/src/DTO/Request/GetLicense/GetLicenseRequest.php @@ -1,3 +1,4 @@ +customerId = $customerId; + } + + /** + * @return string + */ + public function generateRequestUrl() + { + return sprintf('rest/customers/%s/license', $this->customerId); + } -.field-container { - display: flex; - flex-wrap: wrap; + /** + * @return string + */ + public function generateFallbackRequestUrl() + { + return sprintf('rest/customers/%s/license-configuration', $this->customerId); + } } diff --git a/views/css/front/hosted-templates/template2.css b/src/DTO/Request/GetTerminals/GetTerminalsRequest.php similarity index 52% rename from views/css/front/hosted-templates/template2.css rename to src/DTO/Request/GetTerminals/GetTerminalsRequest.php index 8d8d7911a..6e32d45af 100644 --- a/views/css/front/hosted-templates/template2.css +++ b/src/DTO/Request/GetTerminals/GetTerminalsRequest.php @@ -1,3 +1,4 @@ +customerId = $customerId; + } -@media (max-width: 576px) { - .col-sm-8 { - padding-left: 0; + /** + * @return string + */ + public function getCustomerId() + { + return $this->customerId; } -} -#fields-card-number, #fields-expiration, #fields-holder-name, #fields-cvc { - width: 100% !important; -} \ No newline at end of file + /** + * @return string + */ + public function generateRequestUrl() + { + return sprintf('rest/customers/%s/terminals', $this->customerId); + } +} diff --git a/src/Entity/index.php b/src/Entity/index.php deleted file mode 100644 index ee6227264..000000000 --- a/src/Entity/index.php +++ /dev/null @@ -1,31 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); - -header('Location: ../'); -exit; diff --git a/src/Enum/ControllerName.php b/src/Enum/ControllerName.php index d1bfc5f2b..e3118226b 100644 --- a/src/Enum/ControllerName.php +++ b/src/Enum/ControllerName.php @@ -33,15 +33,11 @@ class ControllerName const CREDIT_CARDS = 'creditCards'; const CREDIT_CARDS_16 = 'creditCards16'; const FAIL = 'fail'; - const FAIL_IFRAME = 'failIFrame'; const FAIL_VALIDATION = 'failValidation'; - const HOSTED_IFRAME = 'hostedIframe'; - const IFRAME = 'iframe'; const NOTIFY = 'notify'; const PENDING_NOTIFY = 'pendingNotify'; const SUCCESS = 'success'; const SUCCESS_HOSTED = 'successHosted'; - const SUCCESS_IFRAME = 'successIFrame'; const VALIDATION = 'validation'; const RETURN_URL = 'return'; } diff --git a/src/Enum/PaymentType.php b/src/Enum/PaymentType.php index 865d7c52a..4442ebea8 100644 --- a/src/Enum/PaymentType.php +++ b/src/Enum/PaymentType.php @@ -30,6 +30,5 @@ class PaymentType { const BASIC = 'basic'; - const IFRAME = 'iframe'; const HOSTED_IFRAME = 'hosted_iframe'; } diff --git a/src/Install/AbstractInstaller.php b/src/Install/AbstractInstaller.php index 66c2fbb69..2188841f0 100644 --- a/src/Install/AbstractInstaller.php +++ b/src/Install/AbstractInstaller.php @@ -61,12 +61,7 @@ public function tabs() 'class_name' => SaferPayOfficial::ADMIN_PAYMENTS_CONTROLLER, 'parent_class_name' => SaferPayOfficial::ADMIN_SAFERPAY_MODULE_CONTROLLER, 'module_tab' => true, - ], - [ - 'name' => $this->module->l('Fields'), - 'class_name' => SaferPayOfficial::ADMIN_FIELDS_CONTROLLER, - 'parent_class_name' => SaferPayOfficial::ADMIN_SAFERPAY_MODULE_CONTROLLER, - 'module_tab' => true, + 'visible' => false, ], [ 'name' => $this->module->l('Order'), diff --git a/src/Presentation/Loader/PaymentFormAssetLoader.php b/src/Presentation/Loader/PaymentFormAssetLoader.php index 1c134406c..f4a517ec6 100644 --- a/src/Presentation/Loader/PaymentFormAssetLoader.php +++ b/src/Presentation/Loader/PaymentFormAssetLoader.php @@ -60,7 +60,6 @@ public function register($controller) 'saferpay_official_ajax_url' => $this->context->getLink()->getModuleLink('saferpayofficial', ControllerName::AJAX), 'saferpay_payment_types' => [ 'hosted_iframe' => PaymentType::HOSTED_IFRAME, - 'iframe' => PaymentType::IFRAME, 'basic' => PaymentType::BASIC, ], ]); @@ -154,7 +153,51 @@ private function registerDefaultCheckoutAssets($controller) $controller->registerStylesheet( $this->module->name . '-checkout', - 'modules/' . $this->module->name . '/views/css/front/saferpay_checkout.css' + 'modules/' . $this->module->name . '/views/css/front/saferpay_checkout.css', + ['version' => $this->module->version] + ); + + $this->registerInlineFieldsAssets($controller); + } + + /** + * Registers the Saferpay Fields SDK + inline renderer so Custom-Form cards show their + * card form inline in the default checkout instead of redirecting to a hosted page. + * Only relevant when the account has Fields (Business licence) and a field access token. + * + * @param OrderControllerCore $controller + */ + private function registerInlineFieldsAssets($controller) + { + if (!\Configuration::get(SaferPayConfig::BUSINESS_LICENSE . SaferPayConfig::getConfigSuffix())) { + return; + } + + if (!SaferPayConfig::getFieldAccessToken()) { + return; + } + + Media::addJsDef([ + 'saferpay_field_access_token' => SaferPayConfig::getFieldAccessToken(), + 'saferpay_field_url' => SaferPayConfig::getFieldUrl(), + 'holder_name' => $this->module->l('Holder name', 'PaymentFormAssetLoader'), + 'saferpay_internal_error' => $this->module->l('An error occurred while processing the card, please try again.', 'PaymentFormAssetLoader'), + 'saferpay_fields_incomplete_error' => $this->module->l('Please check the following:', 'PaymentFormAssetLoader'), + 'saferpay_field_label_cardnumber' => $this->module->l('Card number', 'PaymentFormAssetLoader'), + 'saferpay_field_label_expiration' => $this->module->l('Expiry date', 'PaymentFormAssetLoader'), + 'saferpay_field_label_cvc' => $this->module->l('CVC', 'PaymentFormAssetLoader'), + ]); + + $controller->registerJavascript( + 'remote-saferpay-fields-js-lib', + SaferPayConfig::FIELDS_LIBRARY_DEFAULT_VALUE, + ['server' => 'remote', 'position' => 'bottom', 'priority' => 20] + ); + + $controller->registerJavascript( + $this->module->name . '-inline-fields', + 'modules/' . $this->module->name . '/views/js/front/inline-fields.js', + ['position' => 'bottom', 'priority' => 21, 'version' => $this->module->version] ); } diff --git a/src/Provider/PaymentRedirectionProvider.php b/src/Provider/PaymentRedirectionProvider.php index b2f6f1317..f6564e212 100644 --- a/src/Provider/PaymentRedirectionProvider.php +++ b/src/Provider/PaymentRedirectionProvider.php @@ -26,7 +26,6 @@ use Invertus\SaferPay\Adapter\LegacyContext; use Invertus\SaferPay\Config\SaferPayConfig; use Invertus\SaferPay\Enum\ControllerName; -use Invertus\SaferPay\Enum\PaymentType; if (!defined('_PS_VERSION_')) { exit; @@ -39,13 +38,9 @@ class PaymentRedirectionProvider */ private $context; - /** @var PaymentTypeProvider */ - private $paymentTypeProvider; - - public function __construct(LegacyContext $context, PaymentTypeProvider $paymentTypeProvider) + public function __construct(LegacyContext $context) { $this->context = $context; - $this->paymentTypeProvider = $paymentTypeProvider; } /** @@ -55,26 +50,10 @@ public function __construct(LegacyContext $context, PaymentTypeProvider $payment */ public function provideRedirectionLinkByPaymentMethod($paymentMethod) { - $paymentType = $this->paymentTypeProvider->get($paymentMethod); - - if ($paymentType === PaymentType::HOSTED_IFRAME) { - return $this->context->getLink()->getModuleLink( - 'saferpayofficial', - ControllerName::HOSTED_IFRAME, - ['saved_card_method' => $paymentMethod, SaferPayConfig::IS_BUSINESS_LICENCE => true], - true - ); - } - - if ($paymentType === PaymentType::IFRAME) { - return $this->context->getLink()->getModuleLink( - 'saferpayofficial', - ControllerName::IFRAME, - ['saved_card_method' => $paymentMethod, SaferPayConfig::IS_BUSINESS_LICENCE => true], - true - ); - } - + // Card Fields render inline in the checkout (see inline-fields.js), which intercepts + // the submit. This redirect is only the no-JS fallback, so every method falls back to + // the Saferpay Payment Page. The legacy Transaction Interface and the hosted Fields + // page are no longer used. return $this->context->getLink()->getModuleLink( 'saferpayofficial', ControllerName::VALIDATION, diff --git a/src/Provider/PaymentTypeProvider.php b/src/Provider/PaymentTypeProvider.php index 88eb1ac39..7cd7df741 100644 --- a/src/Provider/PaymentTypeProvider.php +++ b/src/Provider/PaymentTypeProvider.php @@ -48,14 +48,13 @@ public function __construct( */ public function get(string $paymentMethod): string { + // Custom Form ON (Saferpay Fields, Business licence) => Saferpay Fields. + // Anything else (Custom Form OFF, non-Business) => Saferpay Payment Page. + // The legacy Transaction Interface (IFRAME) is no longer selectable (SL-374). if ($this->isHostedIframeRedirect($paymentMethod)) { return PaymentType::HOSTED_IFRAME; } - if ($this->isIframeRedirect($paymentMethod)) { - return PaymentType::IFRAME; - } - return PaymentType::BASIC; } @@ -63,30 +62,21 @@ public function get(string $paymentMethod): string * @param string $paymentMethod * @return bool */ - private function isIframeRedirect(string $paymentMethod): bool + private function isHostedIframeRedirect(string $paymentMethod): bool { - if (!in_array($paymentMethod, SaferPayConfig::TRANSACTION_METHODS)) { - return false; - } - if (!\Configuration::get(SaferPayConfig::BUSINESS_LICENSE . SaferPayConfig::getConfigSuffix())) { return false; } - return true; - } - - /** - * @param string $paymentMethod - * @return bool - */ - private function isHostedIframeRedirect(string $paymentMethod): bool - { - if (!$this->saferPayFieldRepository->isActiveByName($paymentMethod)) { - return false; + // Grouped cards render a single inline Fields form under the "Cards" option. + if ($paymentMethod === SaferPayConfig::PAYMENT_CARDS + && \Configuration::get(SaferPayConfig::SAFERPAY_GROUP_CARDS) + ) { + return true; } - if (!\Configuration::get(SaferPayConfig::BUSINESS_LICENSE . SaferPayConfig::getConfigSuffix())) { + // Individual cards use Fields when their "Saferpay Fields" toggle is on. + if (!$this->saferPayFieldRepository->isActiveByName($paymentMethod)) { return false; } diff --git a/src/Repository/SaferPayPaymentRepository.php b/src/Repository/SaferPayPaymentRepository.php index 9b16e20c6..99c48671b 100644 --- a/src/Repository/SaferPayPaymentRepository.php +++ b/src/Repository/SaferPayPaymentRepository.php @@ -110,6 +110,21 @@ public function getActivePaymentMethodsNames() return $result; } + public function getAllPaymentMethodsNames() + { + $query = new DbQuery(); + $query->select('name'); + $query->from('saferpay_payment'); + + $result = Db::getInstance()->executeS($query); + + if (!$result) { + return []; + } + + return $result; + } + public function truncateTable() { $query = 'TRUNCATE TABLE ' . _DB_PREFIX_ . 'saferpay_payment;'; diff --git a/src/Service/Request/RequestObjectCreator.php b/src/Service/Request/RequestObjectCreator.php index d1f2e65d1..65aa0f9cb 100644 --- a/src/Service/Request/RequestObjectCreator.php +++ b/src/Service/Request/RequestObjectCreator.php @@ -120,14 +120,19 @@ public function createPayment(Cart $cart, string $totalPrice): ?Payment $payment = new Payment(); $payment->setValue($totalPrice); $payment->setCurrencyCode($currency['iso_code']); - $payment->setDescription((string) Configuration::get(SaferPayConfig::SAFERPAY_PAYMENT_DESCRIPTION)); - if ((int) \Configuration::get(SaferPayConfig::SAFERPAY_ORDER_CREATION_AFTER_AUTHORIZATION) && empty($order)) { - return $payment; + $description = (string) Configuration::get(SaferPayConfig::SAFERPAY_PAYMENT_DESCRIPTION); + $orderIdOption = (int) Configuration::get(SaferPayConfig::SAFERPAY_ORDER_ID_OPTION); + + if ($orderIdOption === 0 && !empty($order)) { + $payment->setDescription($order->reference); + } else { + $payment->setDescription($description); } - /** This param is not mandatory, but recommended **/ - $payment->setOrderReference($order->reference); + if (!empty($order)) { + $payment->setOrderReference($order->reference); + } return $payment; } diff --git a/src/Service/SaferPayGenerateFieldAccessToken.php b/src/Service/SaferPayGenerateFieldAccessToken.php new file mode 100644 index 000000000..1609cdd37 --- /dev/null +++ b/src/Service/SaferPayGenerateFieldAccessToken.php @@ -0,0 +1,81 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Service; + +use Exception; +use Invertus\SaferPay\Api\Request\GenerateFieldAccessTokenService; +use Invertus\SaferPay\Config\SaferPayConfig; +use Invertus\SaferPay\DTO\Request\GenerateFieldAccessToken\GenerateFieldAccessTokenRequest; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class SaferPayGenerateFieldAccessToken +{ + /** @var GenerateFieldAccessTokenService */ + private $generateFieldAccessTokenService; + + public function __construct(GenerateFieldAccessTokenService $generateFieldAccessTokenService) + { + $this->generateFieldAccessTokenService = $generateFieldAccessTokenService; + } + + /** + * @param string $username + * @param string $password + * @param string $customerId + * @param string $terminalId + * @param bool $isTestMode + * @param string $shopUrl + * + * @return string + * + * @throws Exception + */ + public function generateWithCredentials($username, $password, $customerId, $terminalId, $isTestMode, $shopUrl) + { + $baseUrl = $isTestMode ? SaferPayConfig::TEST_API : SaferPayConfig::API; + $request = new GenerateFieldAccessTokenRequest($customerId, $terminalId); + + $params = [ + 'Description' => 'PrestaShop Module', + 'SourceUrls' => [$shopUrl], + ]; + + $response = $this->generateFieldAccessTokenService->generateToken( + $request, + $username, + $password, + $baseUrl, + $params + ); + + if (!isset($response->AccessToken)) { + throw new Exception('Unexpected API response: no access token returned'); + } + + return $response->AccessToken; + } +} diff --git a/src/Service/SaferPayGetLicense.php b/src/Service/SaferPayGetLicense.php new file mode 100644 index 000000000..a88e410a6 --- /dev/null +++ b/src/Service/SaferPayGetLicense.php @@ -0,0 +1,106 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Service; + +use Exception; +use Invertus\SaferPay\Api\Request\GetLicenseService; +use Invertus\SaferPay\Config\SaferPayConfig; +use Invertus\SaferPay\DTO\Request\GetLicense\GetLicenseRequest; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class SaferPayGetLicense +{ + const FEATURE_HOSTED_ENTRY_FORM = 'HOSTED_ENTRY_FORM'; + + /** @var GetLicenseService */ + private $getLicenseService; + + public function __construct(GetLicenseService $getLicenseService) + { + $this->getLicenseService = $getLicenseService; + } + + /** + * @param string $username + * @param string $password + * @param string $customerId + * @param bool $isTestMode + * + * @return array{hasBusinessLicense: bool, packageName: string, features: array} + * + * @throws Exception + */ + public function fetchLicenseWithCredentials($username, $password, $customerId, $isTestMode) + { + $baseUrl = $isTestMode ? SaferPayConfig::TEST_API : SaferPayConfig::API; + $request = new GetLicenseRequest($customerId); + + $response = $this->fetchWithFallback($request, $username, $password, $baseUrl); + + $packageName = ''; + if (isset($response->Package->DisplayName)) { + $packageName = $response->Package->DisplayName; + } + + $features = []; + $featureList = isset($response->Features) ? $response->Features : []; + if (is_array($featureList)) { + foreach ($featureList as $feature) { + if (isset($feature->Id)) { + $features[] = $feature->Id; + } + } + } + + $hasBusinessLicense = in_array(self::FEATURE_HOSTED_ENTRY_FORM, $features, true); + + return [ + 'hasBusinessLicense' => $hasBusinessLicense, + 'packageName' => $packageName, + 'features' => $features, + ]; + } + + /** + * @param GetLicenseRequest $request + * @param string $username + * @param string $password + * @param string $baseUrl + * + * @return mixed + * + * @throws Exception + */ + private function fetchWithFallback(GetLicenseRequest $request, $username, $password, $baseUrl) + { + try { + return $this->getLicenseService->getLicense($request, $username, $password, $baseUrl); + } catch (Exception $e) { + return $this->getLicenseService->getLicenseFallback($request, $username, $password, $baseUrl); + } + } +} diff --git a/src/Service/SaferPayGetTerminals.php b/src/Service/SaferPayGetTerminals.php new file mode 100644 index 000000000..cb3124bd6 --- /dev/null +++ b/src/Service/SaferPayGetTerminals.php @@ -0,0 +1,111 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Service; + +use Invertus\SaferPay\Api\Request\GetTerminalsService; +use Invertus\SaferPay\Config\SaferPayConfig; +use Invertus\SaferPay\DTO\Request\GetTerminals\GetTerminalsRequest; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class SaferPayGetTerminals +{ + /** + * Terminal types that must never be offered for selection (not usable by this module). + */ + const EXCLUDED_TERMINAL_TYPES = ['MPO', 'SPG']; + + /** @var GetTerminalsService */ + private $getTerminalsService; + + public function __construct(GetTerminalsService $getTerminalsService) + { + $this->getTerminalsService = $getTerminalsService; + } + + /** + * @param string $username + * @param string $password + * @param string $customerId + * @param bool $isTestMode + * @return array + */ + public function fetchTerminalsWithCredentials($username, $password, $customerId, $isTestMode) + { + $baseUrl = $isTestMode ? SaferPayConfig::TEST_API : SaferPayConfig::API; + $request = new GetTerminalsRequest($customerId); + + $response = $this->getTerminalsService->getTerminals( + $request, + $username, + $password, + $baseUrl + ); + + $terminals = []; + $terminalList = isset($response->Terminals) ? $response->Terminals : []; + if (is_array($terminalList)) { + foreach ($terminalList as $terminal) { + if (!isset($terminal->TerminalId)) { + continue; + } + + if (in_array($this->getTerminalType($terminal), self::EXCLUDED_TERMINAL_TYPES, true)) { + continue; + } + + $terminals[] = [ + 'id' => $terminal->TerminalId, + 'name' => isset($terminal->Description) + ? $terminal->Description . ' (' . $terminal->TerminalId . ')' + : $terminal->TerminalId, + ]; + } + } + + return $terminals; + } + + /** + * Reads the terminal type defensively: the Management API has been seen to expose it + * either as "Type" or "TerminalType". Returns an uppercased value (empty when absent). + * + * @param \stdClass $terminal + * @return string + */ + private function getTerminalType($terminal) + { + if (isset($terminal->Type) && is_scalar($terminal->Type)) { + return strtoupper((string) $terminal->Type); + } + + if (isset($terminal->TerminalType) && is_scalar($terminal->TerminalType)) { + return strtoupper((string) $terminal->TerminalType); + } + + return ''; + } +} diff --git a/src/Service/SaferPayOrderStatusService.php b/src/Service/SaferPayOrderStatusService.php index d6ec4f3ad..99dcac232 100644 --- a/src/Service/SaferPayOrderStatusService.php +++ b/src/Service/SaferPayOrderStatusService.php @@ -24,7 +24,6 @@ namespace Invertus\SaferPay\Service; use Cart; -use Customer; use Exception; use Invertus\SaferPay\Adapter\LegacyContext; use Invertus\SaferPay\Api\Enum\TransactionStatus; @@ -32,8 +31,6 @@ use Invertus\SaferPay\Api\Request\CaptureService; use Invertus\SaferPay\Api\Request\RefundService; use Invertus\SaferPay\Config\SaferPayConfig; -use Invertus\SaferPay\DTO\Request\PendingNotification; -use Invertus\SaferPay\Enum\ControllerName; use Invertus\SaferPay\Exception\Api\SaferPayApiException; use Invertus\SaferPay\Factory\ModuleFactory; use Invertus\SaferPay\Logger\LoggerInterface; diff --git a/src/Service/SaferPayTerminalService.php b/src/Service/SaferPayTerminalService.php deleted file mode 100644 index c7f9b9c4a..000000000 --- a/src/Service/SaferPayTerminalService.php +++ /dev/null @@ -1,191 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ - -namespace Invertus\SaferPay\Service; - -use Configuration; -use Exception; -use Invertus\SaferPay\Config\SaferPayConfig; -use Invertus\SaferPay\Logger\LoggerInterface; -use Unirest\Request; - -if (!defined('_PS_VERSION_')) { - exit; -} - -class SaferPayTerminalService -{ - const FILE_NAME = 'SaferPayTerminalService'; - - /** @var LoggerInterface */ - private $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; - } - - /** - * Fetch available terminals from SaferPay REST API - * - * @param string|null $customerId Optional customer ID, if not provided uses config - * @return array Array of terminals with TerminalId and Description - */ - public function getAvailableTerminals($customerId = null) - { - try { - $customerId = $customerId ?: Configuration::get( - SaferPayConfig::CUSTOMER_ID . SaferPayConfig::getConfigSuffix() - ); - - if (empty($customerId)) { - $this->logger->debug(sprintf('%s - Customer ID not configured', self::FILE_NAME)); - return []; - } - - $url = $this->getBaseRestUrl() . '/api/rest/customers/' . $customerId . '/terminals'; - $headers = $this->getHeaders(); - - $this->logger->debug(sprintf('%s - Fetching terminals from: %s', self::FILE_NAME, $url)); - - $request = new Request(); - $response = $request->get($url, $headers); - - $this->logger->debug(sprintf('%s - Terminal API response: %d', self::FILE_NAME, $response->code), [ - 'context' => [ - 'uri' => $url, - ], - 'response' => $response->body, - ]); - - if ($response->code >= 300) { - $this->logger->error(sprintf('%s - Failed to fetch terminals: %d', self::FILE_NAME, $response->code), [ - 'context' => [], - 'response' => $response->body, - ]); - return []; - } - - return $this->parseTerminalsResponse($response->body); - } catch (Exception $exception) { - $this->logger->error(sprintf('%s - Exception: %s', self::FILE_NAME, $exception->getMessage()), [ - 'context' => [], - 'exception' => $exception, - ]); - return []; - } - } - - /** - * Validate if a terminal ID exists in available terminals - * - * @param string $terminalId - * @return bool - */ - public function isValidTerminal($terminalId) - { - if (empty($terminalId)) { - return false; - } - - $terminals = $this->getAvailableTerminals(); - - foreach ($terminals as $terminal) { - if ($terminal['TerminalId'] === $terminalId) { - return true; - } - } - - return false; - } - - /** - * Parse terminals response from API - * - * @param mixed $responseBody - * @return array - */ - private function parseTerminalsResponse($responseBody) - { - $terminals = []; - - if (empty($responseBody)) { - return $terminals; - } - - if (is_object($responseBody)) { - $responseBody = json_decode(json_encode($responseBody), true); - } - - $terminalsList = $responseBody['Terminals'] ?? $responseBody; - - if (is_array($terminalsList)) { - foreach ($terminalsList as $terminal) { - $terminalId = $terminal['TerminalId'] ?? null; - $description = $terminal['Description'] ?? null; - - if ($terminalId) { - $terminals[] = [ - 'TerminalId' => $terminalId, - 'Description' => $description ?: $terminalId, - ]; - } - } - } - - $this->logger->debug(sprintf('%s - Parsed %d terminals', self::FILE_NAME, count($terminals))); - - return $terminals; - } - - /** - * Get REST API base URL - * - * @return string - */ - private function getBaseRestUrl() - { - return SaferPayConfig::getBaseUrl(); - } - - /** - * Get headers for REST API request - * - * @return array - */ - private function getHeaders() - { - $username = Configuration::get(SaferPayConfig::USERNAME . SaferPayConfig::getConfigSuffix()); - $password = Configuration::get(SaferPayConfig::PASSWORD . SaferPayConfig::getConfigSuffix()); - - $credentials = base64_encode("$username:$password"); - - return [ - 'Accept' => 'application/json', - 'Content-Type' => 'application/json', - 'Saferpay-ApiVersion' => SaferPayConfig::API_VERSION, - 'Saferpay-RequestId' => 'false', - 'Authorization' => "Basic $credentials", - ]; - } -} diff --git a/src/Service/SettingsTranslationService.php b/src/Service/SettingsTranslationService.php new file mode 100644 index 000000000..3a68bbf10 --- /dev/null +++ b/src/Service/SettingsTranslationService.php @@ -0,0 +1,266 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Service; + +use Invertus\SaferPay\Factory\ModuleFactory; +use SaferPayOfficial; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class SettingsTranslationService +{ + const FILE_NAME = 'SettingsTranslationService'; + + /** @var SaferPayOfficial */ + private $module; + + public function __construct(ModuleFactory $moduleFactory) + { + $this->module = $moduleFactory->getModule(); + } + + /** + * @return array + */ + public function getAll() + { + return array_merge( + $this->getAppTranslations(), + $this->getTabTranslations(), + $this->getCommonTranslations(), + $this->getApiCredentialsTranslations(), + $this->getPaymentMethodsTranslations(), + $this->getPaymentProcessingTranslations(), + $this->getEmailTranslations(), + $this->getGeneralSettingsTranslations(), + $this->getToastTranslations() + ); + } + + private function getAppTranslations() + { + return [ + 'saferpaySettings' => $this->module->l('Saferpay Settings', self::FILE_NAME), + 'configureIntegration' => $this->module->l('Configure your Saferpay payment integration for your Prestashop store.', self::FILE_NAME), + 'errorLoadingSettings' => $this->module->l('Something went wrong loading Saferpay settings. Please refresh the page.', self::FILE_NAME), + 'failedToLoadSettings' => $this->module->l('Failed to load settings data.', self::FILE_NAME), + ]; + } + + private function getTabTranslations() + { + return [ + 'tabApiCredentials' => $this->module->l('API Credentials', self::FILE_NAME), + 'tabPaymentMethods' => $this->module->l('Payment Methods', self::FILE_NAME), + 'tabPaymentProcessing' => $this->module->l('Payment Processing', self::FILE_NAME), + 'tabEmailNotifications' => $this->module->l('Email Notifications', self::FILE_NAME), + 'tabGeneralSettings' => $this->module->l('General Settings', self::FILE_NAME), + ]; + } + + private function getCommonTranslations() + { + return [ + 'saveChanges' => $this->module->l('Save Changes', self::FILE_NAME), + 'saving' => $this->module->l('Saving...', self::FILE_NAME), + 'enable' => $this->module->l('Enable', self::FILE_NAME), + 'disable' => $this->module->l('Disable', self::FILE_NAME), + 'search' => $this->module->l('Search...', self::FILE_NAME), + 'noResultsFound' => $this->module->l('No results found.', self::FILE_NAME), + 'clearAll' => $this->module->l('Clear all', self::FILE_NAME), + 'selected' => $this->module->l('selected', self::FILE_NAME), + ]; + } + + private function getApiCredentialsTranslations() + { + return [ + 'environment' => $this->module->l('Environment', self::FILE_NAME), + 'envDescription' => $this->module->l('Select your active environment. Credentials are stored separately for each.', self::FILE_NAME), + 'selectEnvironment' => $this->module->l('Select environment', self::FILE_NAME), + 'testEnvironment' => $this->module->l('Test Environment', self::FILE_NAME), + 'liveEnvironment' => $this->module->l('Live Environment', self::FILE_NAME), + 'testModeWarning' => $this->module->l('You are currently in test mode. No real transactions will be processed.', self::FILE_NAME), + 'liveModeWarning' => $this->module->l('You are in live mode. Real transactions will be processed.', self::FILE_NAME), + 'test' => $this->module->l('Test', self::FILE_NAME), + 'live' => $this->module->l('Live', self::FILE_NAME), + 'apiCredentials' => $this->module->l('API Credentials', self::FILE_NAME), + 'enterSaferpayCredentials' => $this->module->l('Enter your Saferpay %s environment API credentials.', self::FILE_NAME), + 'credentialsHint' => html_entity_decode($this->module->l('You can generate your API credentials inside the [backoffice_link] under Settings > JSON API Basic authentication. [more_info_link]', self::FILE_NAME), ENT_QUOTES, 'UTF-8'), + 'credentialsBackofficeLinkText' => $this->module->l('Saferpay Backoffice', self::FILE_NAME), + 'credentialsMoreInfoLinkText' => $this->module->l('More information', self::FILE_NAME), + 'jsonApiUsername' => $this->module->l('JSON API Username', self::FILE_NAME), + 'enterApiUsername' => $this->module->l('Enter %s API username', self::FILE_NAME), + 'jsonApiPassword' => $this->module->l('JSON API Password', self::FILE_NAME), + 'enterApiPassword' => $this->module->l('Enter %s API password', self::FILE_NAME), + 'hidePassword' => $this->module->l('Hide password', self::FILE_NAME), + 'showPassword' => $this->module->l('Show password', self::FILE_NAME), + 'changePassword' => $this->module->l('Change password', self::FILE_NAME), + 'passwordSavedHint' => $this->module->l('Password saved. Click the pencil icon to enter a new one.', self::FILE_NAME), + 'terminalId' => $this->module->l('Terminal ID', self::FILE_NAME), + 'selectTerminal' => $this->module->l('Select a terminal', self::FILE_NAME), + 'refreshTerminals' => $this->module->l('Refresh terminals', self::FILE_NAME), + 'fetchTerminalsFromApi' => $this->module->l('Fetch terminals from API', self::FILE_NAME), + 'merchantEmails' => $this->module->l('Merchant Emails', self::FILE_NAME), + 'enterMerchantEmails' => $this->module->l('Enter merchant email addresses (comma-separated)', self::FILE_NAME), + 'separateEmails' => $this->module->l('These email addresses receive payment notification emails directly from SaferPay. Separate multiple email addresses with commas.', self::FILE_NAME), + 'invalidMerchantEmails' => $this->module->l('Invalid email address', self::FILE_NAME), + 'saferpayFields' => $this->module->l('Saferpay Fields', self::FILE_NAME), + 'saferpayFieldsDescription' => $this->module->l('Configure Saferpay Fields for inline payment form integration.', self::FILE_NAME), + 'fieldAccessTokenInfo' => $this->module->l('Saferpay Field Access Token can be found in Saferpay Backoffice, navigate to', self::FILE_NAME), + 'fieldAccessTokenPath' => $this->module->l('Settings', self::FILE_NAME) . ' > ' . $this->module->l('Saferpay Fields Access Tokens', self::FILE_NAME), + 'fieldAccessToken' => $this->module->l('Field Access Token', self::FILE_NAME), + 'enterFieldAccessToken' => $this->module->l('Enter or generate token', self::FILE_NAME), + 'generate' => $this->module->l('Generate', self::FILE_NAME), + 'enterCredentialsToGenerateToken' => $this->module->l('Enter your API credentials first to generate a token.', self::FILE_NAME), + 'moreInformation' => $this->module->l('More information', self::FILE_NAME), + 'fieldJsUrl' => $this->module->l('Field Javascript Library URL', self::FILE_NAME), + 'findLibraryUrlHere' => $this->module->l('Find the library URL here', self::FILE_NAME), + 'enterCredentialsFirst' => $this->module->l('Enter credentials first', self::FILE_NAME), + 'enterCredentialsToLoadTerminals' => $this->module->l('Enter your API username and password first to load available terminals.', self::FILE_NAME), + 'validatingCredentials' => $this->module->l('Validating credentials...', self::FILE_NAME), + 'credentialsValid' => $this->module->l('Credentials verified successfully.', self::FILE_NAME), + 'invalidCredentials' => $this->module->l('Invalid credentials. Please check your username and password.', self::FILE_NAME), + 'saferpayFieldsIncluded' => $this->module->l('Saferpay Fields is included in your license', self::FILE_NAME), + 'saferpayFieldsIncludedDescription' => $this->module->l('You can use hosted payment fields for a seamless checkout experience.', self::FILE_NAME), + 'tokenGeneratedSuccessfully' => $this->module->l('Access token generated successfully.', self::FILE_NAME), + 'failedToGenerateToken' => $this->module->l('Failed to generate access token.', self::FILE_NAME), + ]; + } + + private function getPaymentMethodsTranslations() + { + return [ + 'paymentMethods' => $this->module->l('Payment Methods', self::FILE_NAME), + 'paymentMethodsDescription' => $this->module->l('Enable and configure available payment methods for your checkout.', self::FILE_NAME), + 'active' => $this->module->l('active', self::FILE_NAME), + 'paymentMethod' => $this->module->l('Payment method', self::FILE_NAME), + 'enabled' => $this->module->l('Enabled', self::FILE_NAME), + 'logos' => $this->module->l('Logos', self::FILE_NAME), + 'customForm' => $this->module->l('Saferpay Fields', self::FILE_NAME), + 'countries' => $this->module->l('Countries', self::FILE_NAME), + 'currencies' => $this->module->l('Currencies', self::FILE_NAME), + 'selectCountries' => $this->module->l('Select countries', self::FILE_NAME), + 'selectCurrencies' => $this->module->l('Select currencies', self::FILE_NAME), + 'select' => $this->module->l('Select', self::FILE_NAME), + 'noPaymentMethods' => $this->module->l('No payment methods available. Please configure your API credentials first.', self::FILE_NAME), + ]; + } + + private function getPaymentProcessingTranslations() + { + return [ + 'transactionHandling' => $this->module->l('Transaction Handling', self::FILE_NAME), + 'transactionHandlingDescription' => $this->module->l('Configure how payments are processed, authorized, and captured.', self::FILE_NAME), + 'defaultPaymentBehavior' => $this->module->l('Default payment behavior', self::FILE_NAME), + 'paymentBehaviorDescription' => $this->module->l('How payment provider should behave when order is created.', self::FILE_NAME), + 'capture' => $this->module->l('Capture', self::FILE_NAME), + 'chargeImmediately' => $this->module->l('Charge immediately', self::FILE_NAME), + 'authorize' => $this->module->l('Authorize', self::FILE_NAME), + 'reserveAndCaptureLater' => $this->module->l('Reserve and capture later', self::FILE_NAME), + 'behaviourWhen3dsFails' => $this->module->l('Behavior when liability shift through 3D Secure has not been granted', self::FILE_NAME), + 'behaviourWhen3dsDescription' => $this->module->l('Default payment behavior for payment without 3-D Secure.', self::FILE_NAME), + 'cancel' => $this->module->l('Cancel', self::FILE_NAME), + 'rejectPayment' => $this->module->l('Reject the payment', self::FILE_NAME), + 'continueWithout3ds' => $this->module->l('Cancel or Capture manually', self::FILE_NAME), + 'captureWithout3ds' => $this->module->l('Charge immediately', self::FILE_NAME), + 'restrictRefundAmount' => $this->module->l('Restrict RefundAmount to Captured Amount', self::FILE_NAME), + 'restrictRefundDescription' => $this->module->l('If set to true, the refund will be rejected if the sum of authorized refunds exceeds the capture value.', self::FILE_NAME), + 'orderCreationRule' => $this->module->l('Order creation rule', self::FILE_NAME), + 'orderCreationDescription' => $this->module->l('Select the option to determine whether the order should be created.', self::FILE_NAME), + 'afterAuthorization' => $this->module->l('After authorization', self::FILE_NAME), + 'createWhenAuthorized' => $this->module->l('Create when authorized', self::FILE_NAME), + 'beforeAuthorization' => $this->module->l('Before authorization', self::FILE_NAME), + 'createBeforePayment' => $this->module->l('Create before payment', self::FILE_NAME), + 'cardDisplaySaving' => html_entity_decode($this->module->l('Card Display & Saving', self::FILE_NAME), ENT_QUOTES, 'UTF-8'), + 'cardDisplay' => $this->module->l('Card Display', self::FILE_NAME), + 'cardDisplayDescription' => $this->module->l('Configure how cards appear and are grouped at checkout.', self::FILE_NAME), + 'cardSavingForCustomers' => $this->module->l('Card Saving for Customers', self::FILE_NAME), + 'groupCardsLabel' => $this->module->l('Group debit/credit cards as \'Cards\' in checkout', self::FILE_NAME), + 'groupCardsDescription' => $this->module->l('If enabled, all supported card brands will be grouped and shown as a single \'Cards\' payment method at checkout.', self::FILE_NAME), + 'showCardsLogo' => $this->module->l('Show \'Cards\' payment method logo', self::FILE_NAME), + 'showCardsLogoDescription' => $this->module->l('If enabled, a logo for the grouped \'Cards\' payment method will be displayed at checkout.', self::FILE_NAME), + 'creditCardSaving' => $this->module->l('Credit card saving for customers', self::FILE_NAME), + 'creditCardSavingDescription' => $this->module->l('Allow customers to save credit card for faster purchase.', self::FILE_NAME), + ]; + } + + private function getEmailTranslations() + { + return [ + 'emailSending' => $this->module->l('Email Sending', self::FILE_NAME), + 'emailSendingDescription' => $this->module->l('Configure which emails are sent during the payment process. Merchant notifications sent by Saferpay use the Merchant Email(s) field on the API Credentials tab.', self::FILE_NAME), + 'saferpayCustomerMail' => $this->module->l('Send an email from Saferpay on payment completion', self::FILE_NAME), + 'saferpayCustomerMailDescription' => $this->module->l('Saferpay sends a payment confirmation email directly to the customer.', self::FILE_NAME), + 'newOrderMail' => $this->module->l('Send new order mail on authorization', self::FILE_NAME), + 'newOrderMailDescription' => $this->module->l('Notify the shop owner when an order is authorized (requires the Mail Alert module).', self::FILE_NAME), + 'orderConfMail' => $this->module->l('Send order confirmation mail on payment completion', self::FILE_NAME), + 'orderConfMailDescription' => $this->module->l('Send the shop\'s order confirmation email to the customer, only once payment is authorized by Saferpay.', self::FILE_NAME), + 'emailConfInfo' => $this->module->l('When this feature is enabled, a confirmation email will be only sent once the payment is authorized by Saferpay.', self::FILE_NAME), + 'emailConfMailAlert' => $this->module->l('For this feature to be functioning you need to have the Mail Alert module configured.', self::FILE_NAME), + ]; + } + + private function getGeneralSettingsTranslations() + { + return [ + 'orderState' => $this->module->l('Order State', self::FILE_NAME), + 'orderStateDescription' => $this->module->l('Define the default order status for Saferpay payments.', self::FILE_NAME), + 'statusAwaitingPayment' => $this->module->l('Status for Saferpay payment awaiting', self::FILE_NAME), + 'selectOrderStatus' => $this->module->l('Select order status', self::FILE_NAME), + 'defaultStatusDescription' => $this->module->l('Default status on SaferPay order creation.', self::FILE_NAME), + 'styling' => $this->module->l('Styling', self::FILE_NAME), + 'stylingDescription' => $this->module->l('Customize the appearance of the payment page.', self::FILE_NAME), + 'configName' => $this->module->l('Payment Page configurations name', self::FILE_NAME), + 'enterConfigName' => $this->module->l('Enter configuration name', self::FILE_NAME), + 'configNameDescription' => html_entity_decode($this->module->l('Name of the Payment Page Configuration created in Saferpay Backoffice (Settings > Payment Page Configuration). Max 20 characters. Allowed: letters, numbers, dots, colons, hyphens, underscores.', self::FILE_NAME), ENT_QUOTES, 'UTF-8'), + 'configuration' => $this->module->l('Configuration', self::FILE_NAME), + 'configurationDescription' => $this->module->l('General module configuration settings.', self::FILE_NAME), + 'description' => $this->module->l('Description', self::FILE_NAME), + 'enterDescription' => $this->module->l('Enter description', self::FILE_NAME), + 'descriptionHelp' => $this->module->l('This description is visible in payment page also in payment confirmation email.', self::FILE_NAME), + 'orderReferenceOnPaymentPage' => $this->module->l('Order reference on payment page', self::FILE_NAME), + 'usePrestaShopOrderReference' => $this->module->l('Use PrestaShop Order reference (default)', self::FILE_NAME), + 'useDescriptionFieldValue' => $this->module->l('Use Description field value', self::FILE_NAME), + 'orderReferenceFallbackInfo' => html_entity_decode($this->module->l('When "Use PrestaShop Order reference" is selected and the order is not yet created (e.g. order creation after authorization), the Description field value is used as fallback.', self::FILE_NAME), ENT_QUOTES, 'UTF-8'), + 'debugMode' => $this->module->l('Debug mode', self::FILE_NAME), + 'debugModeDescription' => $this->module->l('Enable debug mode to see more information in logs.', self::FILE_NAME), + ]; + } + + private function getToastTranslations() + { + return [ + 'failedToFetchTerminals' => $this->module->l('Failed to fetch terminals', self::FILE_NAME), + 'errorFetchingTerminals' => $this->module->l('Error fetching terminals', self::FILE_NAME), + 'savedSuccessfully' => $this->module->l('%s saved successfully', self::FILE_NAME), + 'failedToSave' => $this->module->l('Failed to save %s', self::FILE_NAME), + 'errorSaving' => $this->module->l('Error saving %s: %s', self::FILE_NAME), + 'errorRefreshingPaymentMethods' => $this->module->l('Error refreshing payment methods: %s', self::FILE_NAME), + 'paymentMethodsUnreachable' => $this->module->l('Could not reach your Saferpay account. Please check the error logs for more details', self::FILE_NAME), + ]; + } +} diff --git a/tests/Unit/Service/SaferPayGetTerminalsTest.php b/tests/Unit/Service/SaferPayGetTerminalsTest.php new file mode 100644 index 000000000..878333ed1 --- /dev/null +++ b/tests/Unit/Service/SaferPayGetTerminalsTest.php @@ -0,0 +1,119 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Tests\Unit\Service; + +use Invertus\SaferPay\Api\Request\GetTerminalsService; +use Invertus\SaferPay\Service\SaferPayGetTerminals; +use PHPUnit\Framework\TestCase; + +class SaferPayGetTerminalsTest extends TestCase +{ + /** + * @dataProvider terminalTypePropertyProvider + */ + public function testExcludesMpoAndSpgKeepsOthersAndUntyped($typeProperty) + { + $service = new SaferPayGetTerminals($this->mockGetTerminalsService([ + $this->terminal('MPO_TERMINAL_ID', $typeProperty, 'MPO'), + $this->terminal('SPG_TERMINAL_ID', $typeProperty, 'SPG'), + $this->terminal('OTHER_TERMINAL_ID', $typeProperty, 'EMONEY'), + $this->terminal('NO_TYPE_TERMINAL_ID', $typeProperty, null), + ])); + + $ids = array_column( + $service->fetchTerminalsWithCredentials('u', 'p', 'cust', true), + 'id' + ); + + $this->assertNotContains('MPO_TERMINAL_ID', $ids); + $this->assertNotContains('SPG_TERMINAL_ID', $ids); + $this->assertContains('OTHER_TERMINAL_ID', $ids); + $this->assertContains('NO_TYPE_TERMINAL_ID', $ids); + } + + public function testExclusionIsCaseInsensitive() + { + $service = new SaferPayGetTerminals($this->mockGetTerminalsService([ + $this->terminal('LOWER_MPO', 'Type', 'mpo'), + $this->terminal('MIXED_SPG', 'Type', 'Spg'), + $this->terminal('KEEP', 'Type', 'card'), + ])); + + $ids = array_column( + $service->fetchTerminalsWithCredentials('u', 'p', 'cust', false), + 'id' + ); + + $this->assertSame(['KEEP'], $ids); + } + + public function testPreservesIdAndNameShape() + { + $service = new SaferPayGetTerminals($this->mockGetTerminalsService([ + $this->terminal('T1', 'Type', 'card', 'Main terminal'), + ])); + + $result = $service->fetchTerminalsWithCredentials('u', 'p', 'cust', true); + + $this->assertSame([['id' => 'T1', 'name' => 'Main terminal (T1)']], $result); + } + + public function terminalTypePropertyProvider() + { + // The Management API terminal-type property name is confirmed defensively: + // both "Type" and "TerminalType" are honoured. + return [ + 'Type property' => ['Type'], + 'TerminalType property' => ['TerminalType'], + ]; + } + + private function terminal($id, $typeProperty, $typeValue, $description = null) + { + $terminal = new \stdClass(); + $terminal->TerminalId = $id; + if ($description !== null) { + $terminal->Description = $description; + } + if ($typeValue !== null) { + $terminal->{$typeProperty} = $typeValue; + } + + return $terminal; + } + + private function mockGetTerminalsService(array $terminals) + { + $response = new \stdClass(); + $response->Terminals = $terminals; + + $mock = $this->getMockBuilder(GetTerminalsService::class) + ->disableOriginalConstructor() + ->setMethods(['getTerminals']) + ->getMock(); + $mock->method('getTerminals')->willReturn($response); + + return $mock; + } +} diff --git a/tests/Unit/Service/SaferPayRefreshPaymentsServiceTest.php b/tests/Unit/Service/SaferPayRefreshPaymentsServiceTest.php new file mode 100644 index 000000000..289d5fabd --- /dev/null +++ b/tests/Unit/Service/SaferPayRefreshPaymentsServiceTest.php @@ -0,0 +1,137 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Tests\Unit\Service; + +use Invertus\SaferPay\Logger\LoggerInterface; +use Invertus\SaferPay\Repository\SaferPayFieldRepository; +use Invertus\SaferPay\Repository\SaferPayPaymentRepository; +use Invertus\SaferPay\Repository\SaferPayRestrictionRepository; +use Invertus\SaferPay\Service\SaferPayObtainPaymentMethods; +use Invertus\SaferPay\Service\SaferPayRefreshPaymentsService; +use PHPUnit\Framework\TestCase; + +class SaferPayRefreshPaymentsServiceTest extends TestCase +{ + public function testReconcilePreservesEnabledAddsNewDisabledDropsRemoved() + { + // Stored: VISA (enabled), AMEX (enabled). Account: VISA (kept), TWINT (added), AMEX removed. + $paymentRepository = $this->mockPaymentRepository([ + ['name' => 'VISA', 'active' => '1'], + ['name' => 'AMEX', 'active' => '1'], + ]); + $fieldRepository = $this->mockFieldRepository(['VISA' => true, 'AMEX' => false]); + $obtainPaymentMethods = $this->mockObtainPaymentMethods(['VISA', 'TWINT']); + + // Both tables are rebuilt. + $paymentRepository->expects($this->once())->method('truncateTable'); + $fieldRepository->expects($this->once())->method('truncateTable'); + + // VISA keeps active=1; TWINT added as active=0; AMEX (removed) is never re-inserted. + $paymentRepository->expects($this->exactly(2)) + ->method('insertPayment') + ->withConsecutive( + [['name' => 'VISA', 'active' => 1]], + [['name' => 'TWINT', 'active' => 0]] + ); + // Custom-form flag preserved for VISA (true -> 1), default 0 for the new TWINT. + $fieldRepository->expects($this->exactly(2)) + ->method('insertField') + ->withConsecutive( + [['name' => 'VISA', 'active' => 1]], + [['name' => 'TWINT', 'active' => 0]] + ); + + $this->makeService($paymentRepository, $obtainPaymentMethods, $fieldRepository)->refreshPayments(); + } + + public function testDoesNothingWhenNoActivePaymentMethodsStored() + { + $paymentRepository = $this->mockPaymentRepository([]); + $fieldRepository = $this->mockFieldRepository([]); + $obtainPaymentMethods = $this->mockObtainPaymentMethods(['VISA']); + + // Early return: no API reconciliation and no destructive rebuild. + $obtainPaymentMethods->expects($this->never())->method('obtainPaymentMethodsNamesAsArray'); + $paymentRepository->expects($this->never())->method('truncateTable'); + $paymentRepository->expects($this->never())->method('insertPayment'); + + $this->makeService($paymentRepository, $obtainPaymentMethods, $fieldRepository)->refreshPayments(); + } + + private function makeService($paymentRepository, $obtainPaymentMethods, $fieldRepository) + { + return new SaferPayRefreshPaymentsService( + $paymentRepository, + $obtainPaymentMethods, + $this->createMockWithMethods(SaferPayRestrictionRepository::class, []), + $fieldRepository, + $this->createMockWithMethods(LoggerInterface::class, []) + ); + } + + private function mockPaymentRepository(array $activePayments) + { + $mock = $this->createMockWithMethods( + SaferPayPaymentRepository::class, + ['getActivePaymentMethods', 'truncateTable', 'insertPayment'] + ); + $mock->method('getActivePaymentMethods')->willReturn($activePayments); + + return $mock; + } + + private function mockFieldRepository(array $activeByName) + { + $mock = $this->createMockWithMethods( + SaferPayFieldRepository::class, + ['isActiveByName', 'truncateTable', 'insertField'] + ); + $mock->method('isActiveByName')->willReturnCallback(function ($name) use ($activeByName) { + return isset($activeByName[$name]) ? $activeByName[$name] : false; + }); + + return $mock; + } + + private function mockObtainPaymentMethods(array $names) + { + $mock = $this->createMockWithMethods( + SaferPayObtainPaymentMethods::class, + ['obtainPaymentMethodsNamesAsArray'] + ); + $mock->method('obtainPaymentMethodsNamesAsArray')->willReturn($names); + + return $mock; + } + + private function createMockWithMethods($class, array $methods) + { + $builder = $this->getMockBuilder($class)->disableOriginalConstructor(); + if (!empty($methods)) { + $builder->setMethods($methods); + } + + return $builder->getMock(); + } +} diff --git a/translations/en.php b/translations/en.php new file mode 100644 index 000000000..e69de29bb diff --git a/upgrade/install-1.0.3.php b/upgrade/install-1.0.3.php index a55c92bc2..555e61596 100644 --- a/upgrade/install-1.0.3.php +++ b/upgrade/install-1.0.3.php @@ -41,11 +41,6 @@ function upgrade_module_1_0_3($module) \Invertus\SaferPay\Config\SaferPayConfig::TEST_SUFFIX, \Invertus\SaferPay\Config\SaferPayConfig::FIELDS_LIBRARY_DEFAULT_VALUE ); - Configuration::updateValue( - \Invertus\SaferPay\Config\SaferPayConfig::HOSTED_FIELDS_TEMPLATE, - \Invertus\SaferPay\Config\SaferPayConfig::HOSTED_FIELDS_TEMPLATE_DEFAULT - ); - $result &= Db::getInstance()->execute( 'ALTER TABLE ' . _DB_PREFIX_ . 'saferpay_log MODIFY COLUMN message TEXT NOT NULL, @@ -57,12 +52,5 @@ function upgrade_module_1_0_3($module) ADD COLUMN `authorized` TINYINT(1) DEFAULT 0' ); - $installer = new \Invertus\SaferPay\Install\Installer($module); - $installer->installTab( - SaferPayOfficial::ADMIN_FIELDS_CONTROLLER, - SaferPayOfficial::ADMIN_SAFERPAY_MODULE_CONTROLLER, - $module->l('Fields') - ); - return $result; } diff --git a/upgrade/install-2.1.0.php b/upgrade/install-2.1.0.php new file mode 100644 index 000000000..1fbb4f6d6 --- /dev/null +++ b/upgrade/install-2.1.0.php @@ -0,0 +1,156 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +if (!defined('_PS_VERSION_')) { + exit; +} + +function upgrade_module_2_1_0() +{ + saferpayofficial_2_1_0_delete_removed_tabs(); + saferpayofficial_2_1_0_delete_removed_files(); + saferpayofficial_2_1_0_delete_removed_configuration(); + + Tools::clearSmartyCache(); + + return true; +} + +function saferpayofficial_2_1_0_delete_removed_tabs() +{ + $removedTabs = ['AdminSaferPayOfficialPayment', 'AdminSaferPayOfficialFields']; + + foreach ($removedTabs as $className) { + $tabId = Tab::getIdFromClassName($className); + if (!$tabId) { + continue; + } + + $tab = new Tab($tabId); + $tab->delete(); + } +} + +/** + * A ZIP upgrade overwrites files but never removes the ones a new version dropped, + * so everything deleted in 2.1.0 stays on disk and stays reachable: + * the leftover admin controller is re-registered as a tab by PrestaShop on the next + * module reset, and the leftover iframe front controllers keep answering, one of them + * still reaching the checkout processor with the payment method taken from the request. + * Both fatal on removed class constants, so they can only be cleaned up from here. + */ +function saferpayofficial_2_1_0_delete_removed_files() +{ + $moduleDir = dirname(__DIR__) . DIRECTORY_SEPARATOR; + + $removedFiles = [ + 'controllers/admin/AdminSaferPayOfficialFieldsController.php', + 'controllers/front/failIFrame.php', + 'controllers/front/hostedIframe.php', + 'controllers/front/iframe.php', + 'controllers/front/successIFrame.php', + 'src/Entity/index.php', + 'src/Service/SaferPayTerminalService.php', + 'views/css/admin/saferpay_fields.css', + 'views/css/front/hosted-templates/index.php', + 'views/css/front/hosted-templates/template1.css', + 'views/css/front/hosted-templates/template2.css', + 'views/css/front/hosted-templates/template3.css', + 'views/css/front/saferpay_iframe.css', + 'views/img/example-card/credit-card-back-cvc.png', + 'views/img/example-card/credit-card-back.png', + 'views/img/example-card/credit-card-front-card-number.png', + 'views/img/example-card/credit-card-front-expiration.png', + 'views/img/example-card/credit-card-front.png', + 'views/img/example-card/index.php', + 'views/img/hosted-templates/index.php', + 'views/img/hosted-templates/template1.jpg', + 'views/img/hosted-templates/template2.jpg', + 'views/img/hosted-templates/template3.jpg', + 'views/js/front/hosted-templates/template1.js', + 'views/js/front/hosted-templates/template2.js', + 'views/js/front/hosted-templates/template3.js', + 'views/js/front/hosted-templates/template_submit.js', + 'views/js/front/saferpay_iframe.js', + 'views/templates/admin/field-option-settings/helpers/index.php', + 'views/templates/admin/field-option-settings/helpers/options/index.php', + 'views/templates/admin/field-option-settings/helpers/options/options.tpl', + 'views/templates/admin/field-option-settings/index.php', + 'views/templates/admin/partials/field-hosted-field-template-desc.tpl', + 'views/templates/admin/partials/field-terminal-id.tpl', + 'views/templates/front/hosted-templates/index.php', + 'views/templates/front/hosted-templates/partials/all_errors.tpl', + 'views/templates/front/hosted-templates/partials/all_errors_16.tpl', + 'views/templates/front/hosted-templates/partials/index.php', + 'views/templates/front/hosted-templates/partials/initialize_error.tpl', + 'views/templates/front/hosted-templates/partials/internal_error.tpl', + 'views/templates/front/hosted-templates/partials/submission_error.tpl', + 'views/templates/front/hosted-templates/partials/validation_error.tpl', + 'views/templates/front/hosted-templates/template1.tpl', + 'views/templates/front/hosted-templates/template2.tpl', + 'views/templates/front/hosted-templates/template3.tpl', + 'views/templates/front/saferpay_iframe.tpl', + ]; + + $parentDirectories = []; + + foreach ($removedFiles as $removedFile) { + $path = $moduleDir . str_replace('/', DIRECTORY_SEPARATOR, $removedFile); + + if (!is_file($path)) { + continue; + } + + @unlink($path); + $parentDirectories[dirname($path)] = true; + } + + $parentDirectories = array_keys($parentDirectories); + + usort($parentDirectories, function ($first, $second) { + return strlen($second) - strlen($first); + }); + + foreach ($parentDirectories as $parentDirectory) { + saferpayofficial_2_1_0_delete_empty_directory($parentDirectory, $moduleDir); + } +} + +function saferpayofficial_2_1_0_delete_empty_directory($directory, $moduleDir) +{ + while (strpos($directory, $moduleDir) === 0 && is_dir($directory)) { + $entries = scandir($directory); + + if ($entries === false || count($entries) > 2) { + return; + } + + @rmdir($directory); + $directory = dirname($directory); + } +} + +function saferpayofficial_2_1_0_delete_removed_configuration() +{ + Configuration::deleteByName('SAFERPAY_HOSTED_FIELDS_TEMPLATE'); +} diff --git a/views/css/admin/logs_tab.css b/views/css/admin/logs_tab.css index 5bb36a92f..702cff1ec 100644 --- a/views/css/admin/logs_tab.css +++ b/views/css/admin/logs_tab.css @@ -73,10 +73,31 @@ border-bottom: solid 1px grey; pointer-events: all; display: flex; - justify-content: center; + justify-content: space-between; + align-items: center; max-height: 10vh; } +.log-modal-close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + padding: 0.25rem 0.5rem; + line-height: 1; + color: #6b7280; + margin-right: 0.5rem; +} + +.log-modal-close:hover { + color: #111827; +} + +.log-modal-close:focus { + outline: 2px solid #2196F3; + outline-offset: 2px; +} + .log-modal-content { padding: 15px; height: 50vh; diff --git a/views/css/admin/payment_method.css b/views/css/admin/payment_method.css index 336eda6f0..9b0b08c91 100644 --- a/views/css/admin/payment_method.css +++ b/views/css/admin/payment_method.css @@ -43,6 +43,12 @@ width: 0; } +/* Visible focus indicator for keyboard navigation */ +.container-checkbox input:focus ~ .checkmark { + outline: 2px solid #2196F3; + outline-offset: 2px; +} + /* Create a custom checkbox */ .checkmark { position: absolute; diff --git a/views/css/front/hosted-templates/template1.css b/views/css/front/hosted-templates/template1.css deleted file mode 100644 index 42d99342f..000000000 --- a/views/css/front/hosted-templates/template1.css +++ /dev/null @@ -1,38 +0,0 @@ -/** - *NOTICE OF LICENSE - * - *This source file is subject to the Open Software License (OSL 3.0) - *that is bundled with this package in the file LICENSE.txt. - *It is also available through the world-wide-web at this URL: - *http://opensource.org/licenses/osl-3.0.php - *If you did not receive a copy of the license and are unable to - *obtain it through the world-wide-web, please send an email - *to license@prestashop.com so we can send you a copy immediately. - * - *DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - *versions in the future. If you wish to customize PrestaShop for your - *needs please refer to http://www.prestashop.com for more information. - * - *@author INVERTUS UAB www.invertus.eu - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -#main { - background-color: #fff; - padding: 36px 42px; - border-radius: 36px; - box-shadow: 14px 14px 14px 14px #b3b3b3; - width: 450px; - margin-top: 30px; - margin-bottom: 100px; -} - -#wrapper, #center_column { - height: 600px; -} - -#fields-card-number, #fields-expiration, #fields-holder-name, #fields-cvc { - width: 100% !important; -} diff --git a/views/css/front/hosted-templates/template3.css b/views/css/front/hosted-templates/template3.css deleted file mode 100644 index 1ee756eff..000000000 --- a/views/css/front/hosted-templates/template3.css +++ /dev/null @@ -1,99 +0,0 @@ -/** - *NOTICE OF LICENSE - * - *This source file is subject to the Open Software License (OSL 3.0) - *that is bundled with this package in the file LICENSE.txt. - *It is also available through the world-wide-web at this URL: - *http://opensource.org/licenses/osl-3.0.php - *If you did not receive a copy of the license and are unable to - *obtain it through the world-wide-web, please send an email - *to license@prestashop.com so we can send you a copy immediately. - * - *DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - *versions in the future. If you wish to customize PrestaShop for your - *needs please refer to http://www.prestashop.com for more information. - * - *@author INVERTUS UAB www.invertus.eu - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -.input-box .col { - padding-right: 0; - padding-left: 0; -} - -.input-container, .button-container { - display: flex; - justify-content: center; -} - -.form-control { - border: none; -} - -.submit-button { - width: 100%; -} - -.input-box .col { - padding-right: 5px; - padding-bottom: 5px; -} - -.input-box .col:last-child { - padding-right: 0; - padding-bottom: 0; -} - -@media (max-width: 767px) { - .input-box .col { - padding-right: 0; - } -} - -.credit-card-image { - width: 384px; - height: 249px; -} - -#credit-card { - background: url(../../../img/example-card/credit-card-front.png) no-repeat; - background-size: contain; -} - -.cardnumber { - background: url(../../../img/example-card/credit-card-front-card-number.png) no-repeat !important; -} - -.expiration { - background: url(../../../img/example-card/credit-card-front-expiration.png) no-repeat !important; -} - -.cvc { - background: url(../../../img/example-card/credit-card-back-cvc.png) no-repeat !important; -} - -.image-container { - flex: 1; - display: flex; - justify-content: center; - transition: transform 1s; -} - -.rotate-to-back { - transform: rotateY(-180deg); -} - -.rotate-element { - transform: rotateY(180deg); -} - -#fields-card-number, #fields-expiration, #fields-cvc { - width: 100% !important; -} - -#wrapper, #center_column { - height: 600px; -} \ No newline at end of file diff --git a/views/css/front/saferpay_checkout.css b/views/css/front/saferpay_checkout.css index f7fe67431..d22269c66 100644 --- a/views/css/front/saferpay_checkout.css +++ b/views/css/front/saferpay_checkout.css @@ -51,3 +51,107 @@ input[type="radio"][name^="saved_card_"] { vertical-align: middle !important; margin: 0 !important; } + +/* Saferpay payment-page outlined style: each card field is a fieldset whose legend sits + in a notch on the top border. The card inputs themselves are cross-origin iframes — + their inner styling ships in saferpay-fields-inner.css (loaded into the iframes through + the SDK cssUrl option) — so the visible outline and label are drawn here, on our own + fieldset/legend elements. */ +.saferpay-inline-fields { + max-width: 460px; + padding: 8px 0; +} + +.saferpay-inline-fields .saferpay-field { + min-inline-size: auto; + margin: 0 0 14px; + padding: 0 12px 2px; + border: 1px solid #ccc; + border-radius: 8px; + background: #fff; + transition: border-color .15s ease-in-out; +} + +/* Theme resets: PrestaShop/Bootstrap style legends as full-width block headings. */ +.saferpay-inline-fields .saferpay-field legend { + float: none; + display: block; + width: auto; + margin: 0 0 0 -4px; + padding: 0 4px; + border: none; + font-size: 13px; + font-weight: 400; + line-height: 1; + color: #6f7379; + transition: color .15s ease-in-out; +} + +/* Pre-init placeholder the SDK replaces with its iframe. Same height as the iframe + (the SDK sizes the iframe from the placeholder, and a 0-height placeholder would + yield a 0-height iframe), so nothing flashes or shifts while the fields load. */ +.saferpay-inline-fields .saferpay-field-placeholder { + height: 38px; +} + +.saferpay-inline-fields iframe { + display: block; + width: 100%; + border: none; + box-sizing: border-box; + background: transparent; + opacity: 1; + transition: opacity .2s ease-in; +} + +/* While the SDK initialises, each iframe briefly paints with its default input styling + before our injected stylesheet applies — keep the iframes invisible until the SDK's + init onSuccess lifts this class, leaving the outlined fieldsets as loading skeleton. + An invisible iframe still takes clicks and keystrokes, so without pointer-events a + customer clicking a field during init focuses and types into a field they cannot see: + no caret, no characters, and the field reads as broken. */ +.saferpay-inline-fields.saferpay-fields-loading iframe { + opacity: 0; + pointer-events: none; +} + +/* Two states where a field is drawn but cannot be typed into: while the SDK initialises, and + while the SDK holds the CVC disabled until the card number passes its CheckCard lookup (the + inner input is muted through the injected :disabled rule, see inline-fields.js). Muting the + outline and label too means the field reads as unavailable before it is clicked instead of + after. Declared ahead of the focus/error rules below so both still take precedence. */ +.saferpay-inline-fields.saferpay-fields-loading .saferpay-field, +.saferpay-inline-fields .saferpay-field.is-locked { + border-color: #e0e0e0; + background: #f7f8f8; +} + +.saferpay-inline-fields.saferpay-fields-loading .saferpay-field legend, +.saferpay-inline-fields .saferpay-field.is-locked legend { + color: #9aa4a8; +} + +.saferpay-inline-fields.saferpay-fields-loading .saferpay-field { + cursor: progress; +} + +/* Focus feedback: the SDK's own focus styling is cleared (it shrank the field), so the + Saferpay accent colour is applied to the field outline and its label instead. Colour + only, no inset shadow — a shadow on the fieldset would draw a line across the legend + notch in the top border. */ +.saferpay-inline-fields .saferpay-field.is-focused { + border-color: rgb(39, 119, 119); +} + +.saferpay-inline-fields .saferpay-field.is-focused legend { + color: rgb(39, 119, 119); +} + +/* Highlight a field whose contents are invalid/missing (takes precedence over focus). */ +.saferpay-inline-fields .saferpay-field.has-error { + border-color: #e74c3c; +} + +.saferpay-inline-fields .saferpay-field.has-error legend { + color: #e74c3c; +} diff --git a/views/css/front/saferpay_iframe.css b/views/css/front/saferpay_iframe.css deleted file mode 100644 index 4e2c35c08..000000000 --- a/views/css/front/saferpay_iframe.css +++ /dev/null @@ -1,25 +0,0 @@ -/** - *NOTICE OF LICENSE - * - *This source file is subject to the Open Software License (OSL 3.0) - *that is bundled with this package in the file LICENSE.txt. - *It is also available through the world-wide-web at this URL: - *http://opensource.org/licenses/osl-3.0.php - *If you did not receive a copy of the license and are unable to - *obtain it through the world-wide-web, please send an email - *to license@prestashop.com so we can send you a copy immediately. - * - *DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - *versions in the future. If you wish to customize PrestaShop for your - *needs please refer to http://www.prestashop.com for more information. - * - *@author INVERTUS UAB www.invertus.eu - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -#saferpay-iframe { - width: 100%; - height: 500px; -} \ No newline at end of file diff --git a/views/img/example-card/credit-card-back-cvc.png b/views/img/example-card/credit-card-back-cvc.png deleted file mode 100644 index e0ea287c8..000000000 Binary files a/views/img/example-card/credit-card-back-cvc.png and /dev/null differ diff --git a/views/img/example-card/credit-card-back.png b/views/img/example-card/credit-card-back.png deleted file mode 100644 index a0b502da4..000000000 Binary files a/views/img/example-card/credit-card-back.png and /dev/null differ diff --git a/views/img/example-card/credit-card-front-card-number.png b/views/img/example-card/credit-card-front-card-number.png deleted file mode 100644 index 71d3a64c0..000000000 Binary files a/views/img/example-card/credit-card-front-card-number.png and /dev/null differ diff --git a/views/img/example-card/credit-card-front-expiration.png b/views/img/example-card/credit-card-front-expiration.png deleted file mode 100644 index de2c924bb..000000000 Binary files a/views/img/example-card/credit-card-front-expiration.png and /dev/null differ diff --git a/views/img/example-card/credit-card-front.png b/views/img/example-card/credit-card-front.png deleted file mode 100644 index 3e6191119..000000000 Binary files a/views/img/example-card/credit-card-front.png and /dev/null differ diff --git a/views/img/example-card/index.php b/views/img/example-card/index.php deleted file mode 100644 index ee6227264..000000000 --- a/views/img/example-card/index.php +++ /dev/null @@ -1,31 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); - -header('Location: ../'); -exit; diff --git a/views/img/hosted-templates/index.php b/views/img/hosted-templates/index.php deleted file mode 100644 index ee6227264..000000000 --- a/views/img/hosted-templates/index.php +++ /dev/null @@ -1,31 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); - -header('Location: ../'); -exit; diff --git a/views/img/hosted-templates/template1.jpg b/views/img/hosted-templates/template1.jpg deleted file mode 100644 index 8540a57f2..000000000 Binary files a/views/img/hosted-templates/template1.jpg and /dev/null differ diff --git a/views/img/hosted-templates/template2.jpg b/views/img/hosted-templates/template2.jpg deleted file mode 100644 index c66f2872c..000000000 Binary files a/views/img/hosted-templates/template2.jpg and /dev/null differ diff --git a/views/img/hosted-templates/template3.jpg b/views/img/hosted-templates/template3.jpg deleted file mode 100644 index 7e4876621..000000000 Binary files a/views/img/hosted-templates/template3.jpg and /dev/null differ diff --git a/views/js/admin/log.js b/views/js/admin/log.js index 4a8eb769a..a16cecc4b 100644 --- a/views/js/admin/log.js +++ b/views/js/admin/log.js @@ -21,17 +21,65 @@ */ $(document).ready(function () { + function closeModal($modal) { + $modal.removeClass('open'); + var triggerButton = $modal.data('triggerButton'); + if (triggerButton) { + triggerButton.focus(); + } + } + $('.log-modal-overlay').on('click', function (event) { - $('.modal.open').removeClass('open'); + closeModal($(this).closest('.modal')); + event.preventDefault(); + }); + + $('.js-log-modal-close').on('click', function (event) { + closeModal($(this).closest('.modal')); event.preventDefault(); }); + $(document).on('keydown', function (event) { + var $openModal = $('.modal.open'); + if (!$openModal.length) { + return; + } + if (event.key === 'Escape') { + closeModal($openModal); + event.preventDefault(); + return; + } + if (event.key === 'Tab') { + var focusables = $openModal.find('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])').filter(':visible'); + if (!focusables.length) { + event.preventDefault(); + return; + } + var first = focusables.first()[0]; + var last = focusables.last()[0]; + if (event.shiftKey && document.activeElement === first) { + last.focus(); + event.preventDefault(); + } else if (!event.shiftKey && document.activeElement === last) { + first.focus(); + event.preventDefault(); + } else if (!$openModal[0].contains(document.activeElement)) { + first.focus(); + event.preventDefault(); + } + } + }); + $('.js-log-button').on('click', function (event) { var logId = $(this).data('log-id'); var informationType = $(this).data('information-type'); + var $modal = $('#' + $(this).data('target')); + + $modal.data('triggerButton', $(this)); // NOTE: opening modal - $('#' + $(this).data('target')).addClass('open'); + $modal.addClass('open'); + $modal.find('.js-log-modal-close').focus(); // NOTE: if information has been set already we don't need to call ajax again. if (!$('#log-modal-' + logId + '-' + informationType + ' .log-modal-content-data').hasClass('hidden')) { diff --git a/views/js/admin/saferpay_settings.js b/views/js/admin/saferpay_settings.js index 6407bc177..72f482b10 100644 --- a/views/js/admin/saferpay_settings.js +++ b/views/js/admin/saferpay_settings.js @@ -20,16 +20,23 @@ *@license SIX Payment Services */ -$(document).ready(function (e) { - $("input[name='SAFERPAY_CONFIGURATION_NAME']").keypress(function (e) { - //disable symbols +$(document).ready(function () { + var $configInput = $("input[name='SAFERPAY_CONFIGURATION_NAME']"); + + $configInput.attr('maxlength', 20); + + $configInput.keypress(function (e) { var txt = String.fromCharCode(e.which); - if (!txt.match(/[A-Za-z0-9&. ]/)) { - return false; - } - // disable space - if (e.keyCode === 32) { + if (!txt.match(/[A-Za-z0-9.:\-_]/)) { return false; } }); + + $configInput.on('paste', function (e) { + var $input = $(this); + setTimeout(function () { + var cleaned = $input.val().replace(/[^A-Za-z0-9.:\-_]/g, '').substring(0, 20); + $input.val(cleaned); + }, 0); + }); }); \ No newline at end of file diff --git a/views/js/admin/settings-app/index.html b/views/js/admin/settings-app/index.html new file mode 100644 index 000000000..548a9d367 --- /dev/null +++ b/views/js/admin/settings-app/index.html @@ -0,0 +1,34 @@ + + + + + + SaferPay Settings (Dev) + + +
+ + + + diff --git a/views/js/admin/settings-app/package.json b/views/js/admin/settings-app/package.json new file mode 100644 index 000000000..0498a6630 --- /dev/null +++ b/views/js/admin/settings-app/package.json @@ -0,0 +1,39 @@ +{ + "name": "saferpay-settings-app", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-radio-group": "^1.2.1", + "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "lucide-react": "^0.453.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "tailwind-merge": "^2.5.4" + }, + "devDependencies": { + "vite": "^5.4.10", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "tailwindcss": "^3.4.14", + "postcss": "^8.4.47", + "autoprefixer": "^10.4.20", + "tailwindcss-animate": "^1.0.7" + } +} diff --git a/views/js/admin/settings-app/pnpm-lock.yaml b/views/js/admin/settings-app/pnpm-lock.yaml new file mode 100644 index 000000000..48c426079 --- /dev/null +++ b/views/js/admin/settings-app/pnpm-lock.yaml @@ -0,0 +1,2597 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@radix-ui/react-checkbox': + specifier: ^1.1.2 + version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': + specifier: ^2.1.0 + version: 2.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.2 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: ^1.2.1 + version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.1.2 + version: 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: ^1.1.0 + version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.1.0 + version: 1.2.4(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.1.1 + version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.1.1 + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + class-variance-authority: + specifier: ^0.7.0 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^0.453.0 + version: 0.453.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + tailwind-merge: + specifier: ^2.5.4 + version: 2.6.1 + devDependencies: + '@types/react': + specifier: ^18.3.11 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.28) + '@vitejs/plugin-react': + specifier: ^4.3.3 + version: 4.7.0(vite@5.4.21) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.24(postcss@8.5.6) + postcss: + specifier: ^8.4.47 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.14 + version: 3.4.19 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.19) + typescript: + specifier: ^5.6.3 + version: 5.9.3 + vite: + specifier: ^5.4.10 + version: 5.4.21 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + + '@floating-ui/react-dom@2.1.7': + resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.8': + resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.8': + resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + autoprefixer@10.4.24: + resolution: {integrity: sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001769: + resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.453.0: + resolution: {integrity: sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@2.6.1: + resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.10': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-context@1.1.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-direction@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-id@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-label@2.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-select@2.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-separator@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-slot@1.2.3(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-slot@1.2.4(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/rect@1.1.1': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/estree@1.0.8': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react@18.3.28': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@5.4.21)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21 + transitivePeerDependencies: + - supports-color + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + autoprefixer@10.4.24(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001769 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + baseline-browser-mapping@2.9.19: {} + + binary-extensions@2.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001769 + electron-to-chromium: 1.5.286 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001769: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + + commander@4.1.1: {} + + convert-source-map@2.0.0: {} + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-node-es@1.1.0: {} + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + electron-to-chromium@1.5.286: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fraction.js@5.3.4: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-nonce@1.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.453.0(react@18.3.1): + dependencies: + react: 18.3.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.28)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + react-remove-scroll@2.7.2(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.28)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.28)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.28)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + + react-style-singleton@2.2.3(@types/react@18.3.28)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + source-map-js@1.2.1: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@2.6.1: {} + + tailwindcss-animate@1.0.7(tailwindcss@3.4.19): + dependencies: + tailwindcss: 3.4.19 + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-callback-ref@1.3.3(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + use-sidecar@1.1.3(@types/react@18.3.28)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + util-deprecate@1.0.2: {} + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.57.1 + optionalDependencies: + fsevents: 2.3.3 + + yallist@3.1.1: {} diff --git a/views/js/admin/settings-app/postcss.config.mjs b/views/js/admin/settings-app/postcss.config.mjs new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/views/js/admin/settings-app/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/views/js/admin/settings-app/src/App.tsx b/views/js/admin/settings-app/src/App.tsx new file mode 100644 index 000000000..849fe5f53 --- /dev/null +++ b/views/js/admin/settings-app/src/App.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { SettingsProvider } from './context/settings-context' +import { SaferpaySettings } from './components/settings/saferpay-settings' +import { t } from '@/utils/translations' + +class ErrorBoundary extends React.Component< + { children: React.ReactNode }, + { hasError: boolean } +> { + constructor(props: { children: React.ReactNode }) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError() { + return { hasError: true } + } + + render() { + if (this.state.hasError) { + return ( +
+ {t('errorLoadingSettings')} +
+ ) + } + return this.props.children + } +} + +export default function App() { + return ( + + + + + + ) +} diff --git a/views/js/admin/settings-app/src/api/client.ts b/views/js/admin/settings-app/src/api/client.ts new file mode 100644 index 000000000..9fa4a8149 --- /dev/null +++ b/views/js/admin/settings-app/src/api/client.ts @@ -0,0 +1,85 @@ +import type { PaymentMethodData, TerminalOption } from '@/types' + +interface AjaxResponse { + success: boolean + message?: string +} + +function getConfig() { + return window.saferpaySettingsData +} + +async function postAjax(action: string, data: Record = {}): Promise { + const config = getConfig() + const separator = config.ajaxUrl.includes('?') ? '&' : '?' + const url = `${config.ajaxUrl}${separator}ajax=1&action=${action}&token=${encodeURIComponent(config.adminToken)}` + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify(data), + }) + + if (!response.ok) { + throw new Error(`HTTP error ${response.status}`) + } + + const result = await response.json() + if (typeof result !== 'object' || result === null || typeof result.success !== 'boolean') { + throw new Error('Invalid response format') + } + + return result +} + +export async function saveCredentials(data: Record): Promise { + return postAjax('saveCredentials', data) +} + +export async function savePaymentProcessing(data: Record): Promise { + return postAjax('savePaymentProcessing', data) +} + +export async function saveEmailSettings(data: Record): Promise { + return postAjax('saveEmailSettings', data) +} + +export async function saveGeneralSettings(data: Record): Promise { + return postAjax('saveGeneralSettings', data) +} + +export async function savePaymentMethods(methods: PaymentMethodData[]): Promise { + return postAjax('savePaymentMethods', { paymentMethods: methods }) +} + +export async function getTerminals( + env: string, + username: string, + password: string, +): Promise<{ success: boolean; message?: string; terminals: TerminalOption[] }> { + return postAjax('getTerminals', { env, username, password }) as Promise<{ + success: boolean + message?: string + terminals: TerminalOption[] + }> +} + +export async function generateFieldAccessToken( + env: string, + username: string, + password: string, + terminalId: string, +): Promise<{ success: boolean; message?: string; token?: string }> { + return postAjax('generateFieldAccessToken', { env, username, password, terminalId }) as Promise<{ + success: boolean + message?: string + token?: string + }> +} + +export async function refreshData(): Promise<{ success: boolean; data: Record }> { + return postAjax('refreshData') as Promise<{ success: boolean; data: Record }> +} diff --git a/views/js/admin/settings-app/src/components/settings/api-credentials.tsx b/views/js/admin/settings-app/src/components/settings/api-credentials.tsx new file mode 100644 index 000000000..af037bc12 --- /dev/null +++ b/views/js/admin/settings-app/src/components/settings/api-credentials.tsx @@ -0,0 +1,424 @@ +import { useState, useEffect, useRef } from 'react' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { AlertCircle, Eye, EyeOff, Key, Shield, Loader2, Info, CheckCircle2, Wand2, XCircle, Pencil } from 'lucide-react' +import { useSettings } from '@/context/settings-context' +import { toast } from '@/hooks/use-toast' +import { t } from '@/utils/translations' +import type { TerminalOption } from '@/types' + +type CredentialStatus = 'idle' | 'checking' | 'valid' | 'invalid' + +// Mirrors AdminSaferPayOfficialSettingsController::PASSWORD_PLACEHOLDER. The real +// password is never sent to the browser; a stored credential arrives as this mask. +const PASSWORD_PLACEHOLDER = '********' + +export function ApiCredentials() { + const { settings, updateSettings, saveCredentials, fetchTerminals, generateFieldAccessToken, savingSections } = useSettings() + const saving = savingSections.has('credentials') + const [showApiPassword, setShowApiPassword] = useState(false) + const [generatingToken, setGeneratingToken] = useState(false) + const [terminals, setTerminals] = useState([]) + const [credentialStatus, setCredentialStatus] = useState('idle') + const [credentialError, setCredentialError] = useState('') + const debounceTimer = useRef | null>(null) + + const isTest = settings.testMode + const prefix = isTest ? 'test' : 'live' + const envLabel = isTest ? t('test') : t('live') + const environment = isTest ? 'test' : 'live' + const backofficeUrl = isTest ? 'https://test.saferpay.com/bo/login' : 'https://www.saferpay.com/bo/login' + const jsonApiBasicAuthDocsUrl = 'https://docs.saferpay.com/home/interfaces/backoffice/settings/json-api-basic-client-certificate-authentication#basic-authentication' + + const username = isTest ? settings.testUsername : settings.liveUsername + const password = isTest ? settings.testPassword : settings.livePassword + // While the field still holds the untouched stored-credential mask there is nothing + // to reveal, so the show/hide toggle is hidden until the user types a new password. + const isStoredPasswordMasked = password === PASSWORD_PLACEHOLDER + const terminalId = isTest ? settings.testTerminalId : settings.liveTerminalId + const merchantEmails = isTest ? settings.testMerchantEmails : settings.liveMerchantEmails + const fieldAccessToken = isTest ? settings.testFieldAccessToken : settings.liveFieldAccessToken + const fieldJsUrl = isTest ? settings.testFieldJsUrl : settings.liveFieldJsUrl + + const hasCredentials = username.length > 0 && password.length > 0 + const hasBusinessLicense = isTest ? settings.testHasBusinessLicense : settings.liveHasBusinessLicense + + // The show/hide toggle only appears once the merchant has typed a real password: the field + // must be non-empty and hold something other than the stored mask. + const showPasswordToggle = password.length > 0 && !isStoredPasswordMasked + + const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + const invalidEmails = merchantEmails + .split(',') + .map(e => e.trim()) + .filter(e => e.length > 0 && !EMAIL_RE.test(e)) + const merchantEmailsInvalid = invalidEmails.length > 0 + + const setField = (field: string, value: string | boolean) => { + updateSettings({ [`${prefix}${field.charAt(0).toUpperCase() + field.slice(1)}`]: value } as Record) + } + + // Debounced credential check: when username+password are filled, validate and fetch terminals + useEffect(() => { + if (debounceTimer.current) clearTimeout(debounceTimer.current) + + if (!hasCredentials) { + setCredentialStatus('idle') + setCredentialError('') + return + } + + debounceTimer.current = setTimeout(async () => { + setCredentialStatus('checking') + setCredentialError('') + try { + const result = await fetchTerminals(environment, username, password) + setTerminals(result) + setCredentialStatus('valid') + } catch { + setTerminals([]) + setCredentialStatus('invalid') + setCredentialError(t('invalidCredentials')) + } + }, 1000) + + return () => { + if (debounceTimer.current) clearTimeout(debounceTimer.current) + } + }, [username, password, environment]) + + return ( +
+ {/* Environment Selector */} + + +
+
+ {t('environment')} + {t('envDescription')} +
+ +
+
+ +
+ {isTest ? ( + + ) : ( + + )} + {isTest ? t('testModeWarning') : t('liveModeWarning')} +
+
+
+ + {/* API Credentials */} + + +
+ +
+ + {envLabel} {t('apiCredentials')} + + + {t('enterSaferpayCredentials', envLabel.toLowerCase())} + +
+
+
+ +
+ {/* Credentials generation hint */} +
+ +

+ {t('credentialsHint').split(/(\[backoffice_link\]|\[more_info_link\])/).map((part, index) => { + if (part === '[backoffice_link]') { + return ( + {t('credentialsBackofficeLinkText')} + ) + } + if (part === '[more_info_link]') { + return ( + {t('credentialsMoreInfoLinkText')} + ) + } + return part + })} +

+
+ + {/* Username & Password */} +
+
+ + setField('username', e.target.value)} + data-lpignore="true" + data-1p-ignore="" + data-bwignore="true" + data-form-type="other" + required + aria-required="true" + /> +
+
+ +
+ setField('password', e.target.value)} + readOnly={isStoredPasswordMasked} + className="!sp-pr-10" + data-lpignore="true" + data-1p-ignore="" + data-bwignore="true" + data-form-type="other" + required + aria-required="true" + /> + {isStoredPasswordMasked && ( + + )} + {showPasswordToggle && ( + + )} +
+ {isStoredPasswordMasked && ( +

{t('passwordSavedHint')}

+ )} +
+
+ + {/* Credential status feedback */} + {credentialStatus === 'checking' && ( +
+ + {t('validatingCredentials')} +
+ )} + {credentialStatus === 'valid' && ( +
+ + {t('credentialsValid')} +
+ )} + {credentialStatus === 'invalid' && ( +
+ + {credentialError} +
+ )} + + {/* Terminal ID */} +
+ + + {!hasCredentials && ( +

+ {t('enterCredentialsToLoadTerminals')} +

+ )} +
+ + {/* Merchant Emails */} +
+ + setField('merchantEmails', e.target.value)} + aria-invalid={merchantEmailsInvalid} + className={merchantEmailsInvalid ? 'sp-border-destructive focus-visible:sp-ring-destructive' : ''} + /> + {merchantEmailsInvalid && ( +

+ {t('invalidMerchantEmails')}: {invalidEmails.join(', ')} +

+ )} +

+ {t('separateEmails')} +

+
+
+
+
+ + {hasBusinessLicense && + + {t('saferpayFields')} + {t('saferpayFieldsDescription')} + + +
+
+ +
+

+ {t('saferpayFieldsIncluded')} +

+

+ {t('saferpayFieldsIncludedDescription')} +

+
+
+ +
+ +

+ {t('fieldAccessTokenInfo')}{' '} + {t('fieldAccessTokenPath')}.{' '} + {t('moreInformation')} +

+
+ +
+
+ +
+ setField('fieldAccessToken', e.target.value)} + className="sp-flex-1" + /> + +
+

+ {t('enterCredentialsToGenerateToken')} +

+
+
+ + setField('fieldJsUrl', e.target.value)} + /> + + {t('findLibraryUrlHere')} + +
+
+
+
+
} + +
+ +
+
+ ) +} diff --git a/views/js/admin/settings-app/src/components/settings/email-notifications.tsx b/views/js/admin/settings-app/src/components/settings/email-notifications.tsx new file mode 100644 index 000000000..d79d331ac --- /dev/null +++ b/views/js/admin/settings-app/src/components/settings/email-notifications.tsx @@ -0,0 +1,99 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' +import { Switch } from '@/components/ui/switch' +import { Mail, Info, Loader2 } from 'lucide-react' +import { useSettings } from '@/context/settings-context' +import { t } from '@/utils/translations' + +export function EmailNotifications() { + const { settings, updateSettings, saveEmailSettings, savingSections } = useSettings() + const saving = savingSections.has('emailSettings') + + return ( +
+ + +
+ +
+ {t('emailSending')} + + {t('emailSendingDescription')} + +
+
+
+ +
+
+
+ +

+ {t('saferpayCustomerMailDescription')} +

+
+ updateSettings({ allowSaferpayMail: checked })} + /> +
+ +
+
+ +

+ {t('newOrderMailDescription')} +

+
+ updateSettings({ sendNewOrderMail: checked })} + /> +
+ +
+
+ +

+ {t('orderConfMailDescription')} +

+
+ updateSettings({ sendOrderConfMail: checked })} + /> +
+ +
+ +
+

+ {t('emailConfInfo')} +

+

+ {t('emailConfMailAlert')} +

+
+
+
+
+
+ +
+ +
+
+ ) +} diff --git a/views/js/admin/settings-app/src/components/settings/general-settings.tsx b/views/js/admin/settings-app/src/components/settings/general-settings.tsx new file mode 100644 index 000000000..8e116a3d6 --- /dev/null +++ b/views/js/admin/settings-app/src/components/settings/general-settings.tsx @@ -0,0 +1,188 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Switch } from '@/components/ui/switch' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Settings2, Paintbrush, ClipboardList, Loader2, Info } from 'lucide-react' +import { useSettings } from '@/context/settings-context' +import { t } from '@/utils/translations' + +export function GeneralSettings() { + const { settings, updateSettings, saveGeneralSettings, savingSections } = useSettings() + const saving = savingSections.has('generalSettings') + + return ( +
+ {/* Order State */} + + +
+ +
+ {t('orderState')} + + {t('orderStateDescription')} + +
+
+
+ +
+ + +

+ {t('defaultStatusDescription')} +

+
+
+
+ + {/* Styling */} + + +
+ +
+ {t('styling')} + + {t('stylingDescription')} + +
+
+
+ +
+ + { + const cleaned = e.target.value.replace(/[^A-Za-z0-9.:\-_]/g, '') + updateSettings({ configurationName: cleaned }) + }} + /> +

+ {t('configNameDescription')} +

+
+
+
+ + {/* Configuration */} + + +
+ +
+ {t('configuration')} + + {t('configurationDescription')} + +
+
+
+ +
+ {/* Order reference on payment page */} +
+ + updateSettings({ orderIdOption: Number(val) })} + className="sp-flex sp-flex-col sp-gap-3" + > + + + +
+ + {settings.orderIdOption === 1 && ( +
+ + updateSettings({ paymentDescription: e.target.value })} + /> +

+ {t('descriptionHelp')} +

+
+ )} + + {/* Info banner */} +
+ +

+ {t('orderReferenceFallbackInfo')} +

+
+ +
+
+ +

+ {t('debugModeDescription')} +

+
+ updateSettings({ debugMode: checked })} + /> +
+
+
+
+ +
+ +
+
+ ) +} diff --git a/views/js/admin/settings-app/src/components/settings/payment-methods.tsx b/views/js/admin/settings-app/src/components/settings/payment-methods.tsx new file mode 100644 index 000000000..5a939ff11 --- /dev/null +++ b/views/js/admin/settings-app/src/components/settings/payment-methods.tsx @@ -0,0 +1,342 @@ +import { useState, useCallback, useMemo, useEffect } from 'react' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Switch } from '@/components/ui/switch' +import { Badge } from '@/components/ui/badge' +import { Label } from '@/components/ui/label' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { Checkbox } from '@/components/ui/checkbox' +import { Wallet, ChevronDown, Search, Loader2 } from 'lucide-react' +import { Input } from '@/components/ui/input' +import { useSettings } from '@/context/settings-context' +import { toast } from '@/hooks/use-toast' +import { t } from '@/utils/translations' + +// Module-level so tab switches (which remount the component) don't re-toast. +let fetchFailedToastShown = false + +function MultiSelect({ + options, + selected, + onChange, + placeholder, + label, +}: { + options: Array<{ id: number; name: string }> + selected: number[] + onChange: (values: number[]) => void + placeholder: string + label: string +}) { + const [search, setSearch] = useState('') + + const filteredOptions = useMemo( + () => options.filter((opt) => opt.name.toLowerCase().includes(search.toLowerCase())), + [options, search], + ) + + const ALL_VALUE = 0 + + const validSelected = useMemo( + () => selected.filter((s) => s !== ALL_VALUE && options.some((o) => o.id === s)), + [selected, options], + ) + + const isAll = selected.includes(ALL_VALUE) || validSelected.length === 0 + + const toggle = useCallback( + (value: number) => { + if (value === ALL_VALUE) { + onChange([ALL_VALUE]) + return + } + const base = isAll ? [] : validSelected + const next = base.includes(value) + ? base.filter((s) => s !== value) + : [...base, value] + onChange(next.length === 0 ? [ALL_VALUE] : next) + }, + [validSelected, isAll, onChange], + ) + + return ( + + + + + +
+
+ + setSearch(e.target.value)} + placeholder={t('search')} + className="sp-h-8 sp-pl-7 sp-text-xs" + /> +
+
+
+ {filteredOptions.map((option) => ( + + ))} + {filteredOptions.length === 0 && ( +

+ {t('noResultsFound')} +

+ )} +
+ {validSelected.length > 0 && ( +
+ +
+ )} +
+
+ ) +} + +export function PaymentMethods() { + const { paymentMethods, updatePaymentMethod, savePaymentMethods, savingSections, settings, refreshPaymentMethods } = useSettings() + const saving = savingSections.has('paymentMethods') + + useEffect(() => { + if (paymentMethods.length === 0) { + refreshPaymentMethods() + } + }, [paymentMethods.length, refreshPaymentMethods]) + + // The account check runs while the page bootstraps, so a failure arrives via + // the initial settings data rather than a refresh response. + useEffect(() => { + // Clearing the latch keeps a later, genuine failure from being swallowed. + if (!settings.paymentMethodsFetchFailed) { + fetchFailedToastShown = false + + return + } + + if (!fetchFailedToastShown) { + fetchFailedToastShown = true + toast({ title: t('paymentMethodsUnreachable'), variant: 'warning' }) + } + }, [settings.paymentMethodsFetchFailed]) + + const enabledCount = paymentMethods.filter((m) => m.enabled).length + + return ( +
+ + +
+
+ +
+ {t('paymentMethods')} + + {t('paymentMethodsDescription')} + +
+
+ {enabledCount > 0 && ( + + {enabledCount} {t('active')} + + )} +
+
+ + {/* Header row */} +
+ {t('paymentMethod')} + {t('enabled')} + {t('logos')} + {t('customForm')} + {t('countries')} + {t('currencies')} +
+ + {/* Payment method rows */} +
+ {paymentMethods.map((method) => ( +
+ {/* Desktop layout */} +
+
+ {method.displayName} +
+ +
+ updatePaymentMethod(method.name, { enabled: checked })} + aria-label={`${t('enable')} ${method.displayName}`} + /> +
+ +
+ updatePaymentMethod(method.name, { showLogos: checked })} + aria-label={`${t('logos')} ${method.displayName}`} + /> +
+ +
+ {method.hasCustomForm ? ( + updatePaymentMethod(method.name, { showCustomForm: checked })} + aria-label={`${t('customForm')} ${method.displayName}`} + /> + ) : ( + -- + )} +
+ + updatePaymentMethod(method.name, { countries })} + placeholder={t('selectCountries')} + label={`${t('countries')} ${method.displayName}`} + /> + + ({ id: c.id, name: c.iso_code }))} + selected={method.currencies} + onChange={(currencies) => updatePaymentMethod(method.name, { currencies })} + placeholder={t('selectCurrencies')} + label={`${t('currencies')} ${method.displayName}`} + /> +
+ + {/* Mobile layout */} +
+
+ {method.displayName} + updatePaymentMethod(method.name, { enabled: checked })} + aria-label={`${t('enable')} ${method.displayName}`} + /> +
+ +
+
+ + updatePaymentMethod(method.name, { showLogos: checked })} + aria-label={`${t('logos')} ${method.displayName}`} + /> +
+ {method.hasCustomForm && ( +
+ + updatePaymentMethod(method.name, { showCustomForm: checked })} + aria-label={`${t('customForm')} ${method.displayName}`} + /> +
+ )} +
+ +
+
+ + updatePaymentMethod(method.name, { countries })} + placeholder={t('select')} + label={`${t('countries')} ${method.displayName}`} + /> +
+
+ + ({ id: c.id, name: c.iso_code }))} + selected={method.currencies} + onChange={(currencies) => updatePaymentMethod(method.name, { currencies })} + placeholder={t('select')} + label={`${t('currencies')} ${method.displayName}`} + /> +
+
+
+
+ ))} + + {paymentMethods.length === 0 && ( +
+ {t('noPaymentMethods')} +
+ )} +
+
+
+ + {paymentMethods.length > 0 && ( +
+ +
+ )} +
+ ) +} diff --git a/views/js/admin/settings-app/src/components/settings/payment-processing.tsx b/views/js/admin/settings-app/src/components/settings/payment-processing.tsx new file mode 100644 index 000000000..95ab28d43 --- /dev/null +++ b/views/js/admin/settings-app/src/components/settings/payment-processing.tsx @@ -0,0 +1,326 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' +import { Switch } from '@/components/ui/switch' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' +import { CreditCard, ShieldCheck, Loader2 } from 'lucide-react' +import { useSettings } from '@/context/settings-context' +import { t } from '@/utils/translations' + +export function PaymentProcessing() { + const { settings, updateSettings, savePaymentProcessing, savingSections } = useSettings() + const saving = savingSections.has('paymentProcessing') + + return ( +
+ {/* Transaction Handling */} + + +
+ +
+ {t('transactionHandling')} + + {t('transactionHandlingDescription')} + +
+
+
+ +
+ {/* Default Payment Behavior */} +
+
+ +

+ {t('paymentBehaviorDescription')} +

+
+ updateSettings({ paymentBehavior: Number(val) })} + className="sp-flex sp-gap-3" + > + + + +
+ + {/* 3D Secure Behavior */} +
+
+ +

+ {t('behaviourWhen3dsDescription')} +

+
+ updateSettings({ paymentBehaviorWithout3D: Number(val) })} + className="sp-flex sp-gap-3" + > + + + + +
+ + {/* Restrict Refund */} +
+
+ +

+ {t('restrictRefundDescription')} +

+
+ updateSettings({ restrictRefund: Number(val) })} + className="sp-flex sp-gap-3" + > + + + +
+ + {/* Order Creation Rule */} +
+
+ +

+ {t('orderCreationDescription')} +

+
+ updateSettings({ orderCreationAfterAuth: Number(val) })} + className="sp-flex sp-gap-3" + > + + + +
+
+
+
+ + {/* Card Display */} + + +
+ +
+ {t('cardDisplay')} + + {t('cardDisplayDescription')} + +
+
+
+ +
+
+
+ +

+ {t('groupCardsDescription')} +

+
+ updateSettings({ groupCards: checked })} + /> +
+ + {settings.groupCards && ( +
+
+ +

+ {t('showCardsLogoDescription')} +

+
+
+ )} +
+
+
+ + {/* Card Saving for Customers */} + + +
+ +
+ {t('cardSavingForCustomers')} + + {t('creditCardSavingDescription')} + +
+
+
+ +
+
+ updateSettings({ creditCardSave: Number(val) })} + className="sp-flex sp-gap-3" + > + + + +
+
+
+
+ +
+ +
+
+ ) +} diff --git a/views/js/admin/settings-app/src/components/settings/saferpay-settings.tsx b/views/js/admin/settings-app/src/components/settings/saferpay-settings.tsx new file mode 100644 index 000000000..93b02197c --- /dev/null +++ b/views/js/admin/settings-app/src/components/settings/saferpay-settings.tsx @@ -0,0 +1,91 @@ +import { t } from '@/utils/translations' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { ApiCredentials } from './api-credentials' +import { PaymentProcessing } from './payment-processing' +import { PaymentMethods } from './payment-methods' +import { EmailNotifications } from './email-notifications' +import { GeneralSettings } from './general-settings' +import { ToastContainer } from './toast-container' +import { Key, CreditCard, Wallet, Mail, Settings2 } from 'lucide-react' + +export function SaferpaySettings() { + return ( +
+
+

+ {t('saferpaySettings')} +

+

+ {t('configureIntegration')} +

+
+ + + + + + {t('tabApiCredentials')} + + + + {t('tabPaymentMethods')} + + + + {t('tabPaymentProcessing')} + + + + {t('tabEmailNotifications')} + + + + {t('tabGeneralSettings')} + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} diff --git a/views/js/admin/settings-app/src/components/settings/toast-container.tsx b/views/js/admin/settings-app/src/components/settings/toast-container.tsx new file mode 100644 index 000000000..0b5f905db --- /dev/null +++ b/views/js/admin/settings-app/src/components/settings/toast-container.tsx @@ -0,0 +1,29 @@ +import { useToast } from '@/hooks/use-toast' + +export function ToastContainer() { + const { toasts } = useToast() + + return ( +
+ {toasts.map((t) => ( +
+ {t.title &&
{t.title}
} + {t.description &&
{t.description}
} +
+ ))} +
+ ) +} diff --git a/views/js/admin/settings-app/src/components/ui/alert.tsx b/views/js/admin/settings-app/src/components/ui/alert.tsx new file mode 100644 index 000000000..b88b9053f --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const alertVariants = cva( + 'sp-relative sp-w-full sp-rounded-lg sp-border sp-p-4 [&>svg~*]:sp-pl-7 [&>svg+div]:sp-translate-y-[-3px] [&>svg]:sp-absolute [&>svg]:sp-left-4 [&>svg]:sp-top-4 [&>svg]:sp-text-foreground', + { + variants: { + variant: { + default: 'sp-bg-background sp-text-foreground', + destructive: + 'sp-border-destructive/50 sp-text-destructive [&>svg]:sp-text-destructive', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = 'Alert' + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = 'AlertTitle' + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = 'AlertDescription' + +export { Alert, AlertTitle, AlertDescription } diff --git a/views/js/admin/settings-app/src/components/ui/badge.tsx b/views/js/admin/settings-app/src/components/ui/badge.tsx new file mode 100644 index 000000000..7bafa84a1 --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/badge.tsx @@ -0,0 +1,34 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'sp-inline-flex sp-items-center sp-rounded-full sp-border sp-px-2.5 sp-py-0.5 sp-text-xs sp-font-semibold sp-transition-colors focus:sp-outline-none focus:sp-ring-2 focus:sp-ring-ring focus:sp-ring-offset-2', + { + variants: { + variant: { + default: 'sp-border-transparent sp-bg-primary sp-text-primary-foreground hover:sp-bg-primary/80', + secondary: 'sp-border-transparent sp-bg-secondary sp-text-secondary-foreground hover:sp-bg-secondary/80', + destructive: 'sp-border-transparent sp-bg-destructive sp-text-destructive-foreground hover:sp-bg-destructive/80', + outline: 'sp-text-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +export interface BadgeProps + extends + React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/views/js/admin/settings-app/src/components/ui/button.tsx b/views/js/admin/settings-app/src/components/ui/button.tsx new file mode 100644 index 000000000..a71b3fbd1 --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/button.tsx @@ -0,0 +1,58 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "sp-inline-flex sp-items-center sp-justify-center sp-gap-2 sp-whitespace-nowrap sp-rounded-md sp-text-sm sp-font-medium sp-ring-offset-background sp-transition-colors focus-visible:sp-outline-none focus-visible:sp-ring-2 focus-visible:sp-ring-ring focus-visible:sp-ring-offset-2 disabled:sp-pointer-events-none disabled:sp-opacity-50 [&_svg]:sp-pointer-events-none [&_svg]:sp-size-4 [&_svg]:sp-shrink-0", + { + variants: { + variant: { + default: + "sp-bg-primary sp-text-primary-foreground hover:sp-bg-primary/90", + destructive: + "sp-bg-destructive sp-text-destructive-foreground hover:sp-bg-destructive/90", + outline: + "sp-border sp-border-input sp-bg-background hover:sp-bg-accent hover:sp-text-accent-foreground", + secondary: + "sp-bg-secondary sp-text-secondary-foreground hover:sp-bg-secondary/80", + ghost: "hover:sp-bg-accent hover:sp-text-accent-foreground", + link: "sp-text-primary sp-underline-offset-4 hover:sp-underline", + }, + size: { + default: "sp-h-10 sp-px-4 sp-py-2", + sm: "sp-h-9 sp-rounded-md sp-px-3", + lg: "sp-h-11 sp-rounded-md sp-px-8", + icon: "sp-h-10 sp-w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends + React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/views/js/admin/settings-app/src/components/ui/card.tsx b/views/js/admin/settings-app/src/components/ui/card.tsx new file mode 100644 index 000000000..aa1bc9ff4 --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = 'CardTitle' + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = 'CardFooter' + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/views/js/admin/settings-app/src/components/ui/checkbox.tsx b/views/js/admin/settings-app/src/components/ui/checkbox.tsx new file mode 100644 index 000000000..2ff67ed97 --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' +import * as CheckboxPrimitive from '@radix-ui/react-checkbox' +import { Check } from 'lucide-react' + +import { cn } from '@/lib/utils' + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/views/js/admin/settings-app/src/components/ui/input.tsx b/views/js/admin/settings-app/src/components/ui/input.tsx new file mode 100644 index 000000000..1ae8b9178 --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + }, +) +Input.displayName = 'Input' + +export { Input } diff --git a/views/js/admin/settings-app/src/components/ui/label.tsx b/views/js/admin/settings-app/src/components/ui/label.tsx new file mode 100644 index 000000000..94983e39e --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const labelVariants = cva( + 'sp-text-sm sp-font-medium sp-leading-none peer-disabled:sp-cursor-not-allowed peer-disabled:sp-opacity-70', +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/views/js/admin/settings-app/src/components/ui/popover.tsx b/views/js/admin/settings-app/src/components/ui/popover.tsx new file mode 100644 index 000000000..b41db63a5 --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/popover.tsx @@ -0,0 +1,29 @@ +import * as React from 'react' +import * as PopoverPrimitive from '@radix-ui/react-popover' + +import { cn } from '@/lib/utils' + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + ('.sp-saferpay-root')}> + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/views/js/admin/settings-app/src/components/ui/radio-group.tsx b/views/js/admin/settings-app/src/components/ui/radio-group.tsx new file mode 100644 index 000000000..e3376efd5 --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/radio-group.tsx @@ -0,0 +1,42 @@ +import * as React from 'react' +import * as RadioGroupPrimitive from '@radix-ui/react-radio-group' +import { Circle } from 'lucide-react' + +import { cn } from '@/lib/utils' + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/views/js/admin/settings-app/src/components/ui/select.tsx b/views/js/admin/settings-app/src/components/ui/select.tsx new file mode 100644 index 000000000..d64df3b54 --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/select.tsx @@ -0,0 +1,156 @@ +import * as React from 'react' +import * as SelectPrimitive from '@radix-ui/react-select' +import { Check, ChevronDown, ChevronUp } from 'lucide-react' + +import { cn } from '@/lib/utils' + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:sp-line-clamp-1', + className, + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + ('.sp-saferpay-root')}> + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/views/js/admin/settings-app/src/components/ui/separator.tsx b/views/js/admin/settings-app/src/components/ui/separator.tsx new file mode 100644 index 000000000..c4a67a9cc --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/separator.tsx @@ -0,0 +1,29 @@ +import * as React from 'react' +import * as SeparatorPrimitive from '@radix-ui/react-separator' + +import { cn } from '@/lib/utils' + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = 'horizontal', decorative = true, ...props }, + ref, + ) => ( + + ), +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/views/js/admin/settings-app/src/components/ui/switch.tsx b/views/js/admin/settings-app/src/components/ui/switch.tsx new file mode 100644 index 000000000..081dce0fd --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from 'react' +import * as SwitchPrimitives from '@radix-ui/react-switch' + +import { cn } from '@/lib/utils' + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/views/js/admin/settings-app/src/components/ui/tabs.tsx b/views/js/admin/settings-app/src/components/ui/tabs.tsx new file mode 100644 index 000000000..4dd5a0f38 --- /dev/null +++ b/views/js/admin/settings-app/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from 'react' +import * as TabsPrimitive from '@radix-ui/react-tabs' + +import { cn } from '@/lib/utils' + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/views/js/admin/settings-app/src/context/settings-context.tsx b/views/js/admin/settings-app/src/context/settings-context.tsx new file mode 100644 index 000000000..7069b1dbb --- /dev/null +++ b/views/js/admin/settings-app/src/context/settings-context.tsx @@ -0,0 +1,241 @@ +import React, { createContext, useContext, useState, useCallback, useMemo, useRef } from 'react' +import type { SaferpaySettingsData, PaymentMethodData, TerminalOption } from '@/types' +import * as api from '@/api/client' +import { toast } from '@/hooks/use-toast' +import { t } from '@/utils/translations' + +type SavingSection = 'credentials' | 'paymentProcessing' | 'emailSettings' | 'generalSettings' | 'paymentMethods' + +interface SettingsContextValue { + settings: SaferpaySettingsData + updateSettings: (updates: Partial) => void + saveCredentials: () => Promise + savePaymentProcessing: () => Promise + saveEmailSettings: () => Promise + saveGeneralSettings: () => Promise + savePaymentMethods: () => Promise + fetchTerminals: (env: string, username: string, password: string) => Promise + generateFieldAccessToken: () => Promise<{ success: boolean; message?: string; token?: string }> + refreshPaymentMethods: () => Promise + paymentMethods: PaymentMethodData[] + updatePaymentMethod: (name: string, updates: Partial) => void + savingSections: Set +} + +const SettingsContext = createContext(null) + +export function SettingsProvider({ children }: { children: React.ReactNode }) { + const [settings, setSettings] = useState(() => window.saferpaySettingsData) + const [paymentMethods, setPaymentMethods] = useState( + () => Array.isArray(window.saferpaySettingsData.paymentMethods) ? window.saferpaySettingsData.paymentMethods : [], + ) + const [savingSections, setSavingSections] = useState>(new Set()) + + const settingsRef = useRef(settings) + settingsRef.current = settings + + const paymentMethodsRef = useRef(paymentMethods) + paymentMethodsRef.current = paymentMethods + + const updateSettings = useCallback((updates: Partial) => { + setSettings((prev) => ({ ...prev, ...updates })) + }, []) + + const updatePaymentMethod = useCallback((name: string, updates: Partial) => { + setPaymentMethods((prev) => + prev.map((m) => (m.name === name ? { ...m, ...updates } : m)), + ) + }, []) + + const handleSave = useCallback(async ( + saveFn: () => Promise<{ success: boolean; message?: string; warning?: boolean }>, + label: string, + section: SavingSection, + ) => { + setSavingSections((prev) => new Set(prev).add(section)) + try { + const result = await saveFn() + if (result.success) { + const variant = result.warning ? 'warning' : 'default' + toast({ title: result.message || t('savedSuccessfully', label), variant }) + } else { + toast({ title: result.message || t('failedToSave', label), variant: 'destructive' }) + } + } catch (e) { + const message = e instanceof Error ? e.message : 'Unknown error' + toast({ title: t('errorSaving', label, message), variant: 'destructive' }) + } finally { + setSavingSections((prev) => { + const next = new Set(prev) + next.delete(section) + return next + }) + } + }, []) + + const saveCredentials = useCallback(async () => { + const currentSettings = settingsRef.current + await handleSave(async () => { + const result = await api.saveCredentials({ + testMode: currentSettings.testMode, + testUsername: currentSettings.testUsername, + testPassword: currentSettings.testPassword, + testTerminalId: currentSettings.testTerminalId, + testMerchantEmails: currentSettings.testMerchantEmails, + testFieldAccessToken: currentSettings.testFieldAccessToken, + testFieldJsUrl: currentSettings.testFieldJsUrl, + liveUsername: currentSettings.liveUsername, + livePassword: currentSettings.livePassword, + liveTerminalId: currentSettings.liveTerminalId, + liveMerchantEmails: currentSettings.liveMerchantEmails, + liveFieldAccessToken: currentSettings.liveFieldAccessToken, + liveFieldJsUrl: currentSettings.liveFieldJsUrl, + }) + const data = result as unknown as Record + if (result.success) { + setSettings((prev) => ({ + ...prev, + ...(typeof data.testHasBusinessLicense === 'boolean' ? { testHasBusinessLicense: data.testHasBusinessLicense as boolean } : {}), + ...(typeof data.liveHasBusinessLicense === 'boolean' ? { liveHasBusinessLicense: data.liveHasBusinessLicense as boolean } : {}), + })) + } + return { ...result, warning: data.warning === true } + }, 'API Credentials', 'credentials') + }, [handleSave]) + + const savePaymentProcessing = useCallback(async () => { + const currentSettings = settingsRef.current + await handleSave(() => api.savePaymentProcessing({ + paymentBehavior: currentSettings.paymentBehavior, + paymentBehaviorWithout3D: currentSettings.paymentBehaviorWithout3D, + restrictRefund: currentSettings.restrictRefund, + orderCreationAfterAuth: currentSettings.orderCreationAfterAuth, + groupCards: currentSettings.groupCards, + groupCardsLogo: currentSettings.groupCardsLogo, + creditCardSave: currentSettings.creditCardSave, + }), 'Payment Processing', 'paymentProcessing') + }, [handleSave]) + + const saveEmailSettings = useCallback(async () => { + const currentSettings = settingsRef.current + await handleSave(() => api.saveEmailSettings({ + allowSaferpayMail: currentSettings.allowSaferpayMail, + sendNewOrderMail: currentSettings.sendNewOrderMail, + sendOrderConfMail: currentSettings.sendOrderConfMail, + }), 'Email Settings', 'emailSettings') + }, [handleSave]) + + const saveGeneralSettings = useCallback(async () => { + const currentSettings = settingsRef.current + await handleSave(() => api.saveGeneralSettings({ + orderStateAwaitingPayment: currentSettings.orderStateAwaitingPayment, + paymentDescription: currentSettings.paymentDescription, + configurationName: currentSettings.configurationName, + orderIdOption: currentSettings.orderIdOption, + debugMode: currentSettings.debugMode, + }), 'General Settings', 'generalSettings') + }, [handleSave]) + + const savePaymentMethods = useCallback(async () => { + await handleSave( + () => api.savePaymentMethods(paymentMethodsRef.current), + 'Payment Methods', + 'paymentMethods', + ) + }, [handleSave]) + + const refreshPaymentMethods = useCallback(async () => { + try { + const result = await api.refreshData() + if (result.success && Array.isArray(result.data?.paymentMethods)) { + setPaymentMethods(result.data.paymentMethods as PaymentMethodData[]) + } + + // Mirror the refreshed outcome onto the bootstrap flag, otherwise a failure from + // page load keeps warning after a later refresh has already succeeded. Only a + // response that actually reports the flag may clear it. + if (result.data && 'paymentMethodsFetchFailed' in result.data) { + const fetchFailed = result.data.paymentMethodsFetchFailed === true + setSettings((prev) => ({ ...prev, paymentMethodsFetchFailed: fetchFailed })) + + if (fetchFailed) { + toast({ title: t('paymentMethodsUnreachable'), variant: 'warning' }) + } + } + } catch { + toast({ title: t('errorRefreshingPaymentMethods'), variant: 'destructive' }) + } + }, []) + + const generateFieldAccessToken = useCallback(async () => { + const s = settingsRef.current + const env = s.testMode ? 'test' : 'live' + const username = s.testMode ? s.testUsername : s.liveUsername + const password = s.testMode ? s.testPassword : s.livePassword + const terminalId = s.testMode ? s.testTerminalId : s.liveTerminalId + + const result = await api.generateFieldAccessToken(env, username, password, terminalId) + if (!result.success) { + throw new Error(result.message || t('failedToGenerateToken')) + } + + if (result.token) { + const fieldKey = s.testMode ? 'testFieldAccessToken' : 'liveFieldAccessToken' + setSettings((prev) => ({ ...prev, [fieldKey]: result.token })) + } + + return result + }, []) + + const fetchTerminals = useCallback(async (env: string, username: string, password: string) => { + const result = await api.getTerminals(env, username, password) + if (!result.success) { + throw new Error(result.message || t('failedToFetchTerminals')) + } + return result.terminals + }, []) + + const value = useMemo(() => ({ + settings, + updateSettings, + saveCredentials, + savePaymentProcessing, + saveEmailSettings, + saveGeneralSettings, + savePaymentMethods, + fetchTerminals, + generateFieldAccessToken, + refreshPaymentMethods, + paymentMethods, + updatePaymentMethod, + savingSections, + }), [ + settings, + updateSettings, + saveCredentials, + savePaymentProcessing, + saveEmailSettings, + saveGeneralSettings, + savePaymentMethods, + fetchTerminals, + generateFieldAccessToken, + refreshPaymentMethods, + paymentMethods, + updatePaymentMethod, + savingSections, + ]) + + return ( + + {children} + + ) +} + +export function useSettings() { + const context = useContext(SettingsContext) + if (!context) { + throw new Error('useSettings must be used within a SettingsProvider') + } + return context +} diff --git a/views/js/admin/settings-app/src/globals.css b/views/js/admin/settings-app/src/globals.css new file mode 100644 index 000000000..b222286c7 --- /dev/null +++ b/views/js/admin/settings-app/src/globals.css @@ -0,0 +1,42 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.sp-saferpay-root { + --sp-background: 210 20% 97%; + --sp-foreground: 220 20% 10%; + --sp-card: 0 0% 100%; + --sp-card-foreground: 220 20% 10%; + --sp-popover: 0 0% 100%; + --sp-popover-foreground: 220 20% 10%; + --sp-primary: 180 51% 31%; + --sp-primary-foreground: 0 0% 100%; + --sp-secondary: 180 15% 95%; + --sp-secondary-foreground: 220 20% 10%; + --sp-muted: 180 12% 95%; + --sp-muted-foreground: 220 12% 38%; + --sp-accent: 180 25% 92%; + --sp-accent-foreground: 180 51% 22%; + --sp-destructive: 0 72% 51%; + --sp-destructive-foreground: 0 0% 100%; + --sp-border: 214 20% 90%; + --sp-input: 214 20% 90%; + --sp-ring: 180 51% 31%; + --sp-radius: 0.625rem; + + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + font-size: 14px; + line-height: 1.5; + color: hsl(var(--sp-foreground)); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.sp-saferpay-root *, +.sp-saferpay-root *::before, +.sp-saferpay-root *::after { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid hsl(var(--sp-border)); +} diff --git a/views/js/admin/settings-app/src/hooks/use-toast.ts b/views/js/admin/settings-app/src/hooks/use-toast.ts new file mode 100644 index 000000000..f0af5fe04 --- /dev/null +++ b/views/js/admin/settings-app/src/hooks/use-toast.ts @@ -0,0 +1,103 @@ +import * as React from 'react' + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 5000 + +export interface ToasterToast { + id: string + title?: string + description?: string + variant?: 'default' | 'destructive' | 'warning' +} + +type Action = + | { type: 'ADD_TOAST'; toast: ToasterToast } + | { type: 'UPDATE_TOAST'; toast: Partial & { id: string } } + | { type: 'DISMISS_TOAST'; toastId?: string } + | { type: 'REMOVE_TOAST'; toastId?: string } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +let count = 0 +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +function addToRemoveQueue(toastId: string) { + if (toastTimeouts.has(toastId)) return + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ type: 'REMOVE_TOAST', toastId }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case 'ADD_TOAST': + return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT) } + case 'UPDATE_TOAST': + return { ...state, toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)) } + case 'DISMISS_TOAST': { + const { toastId } = action + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((t) => addToRemoveQueue(t.id)) + } + return state + } + case 'REMOVE_TOAST': + if (action.toastId === undefined) return { ...state, toasts: [] } + return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId) } + } +} + +const listeners: Array<(state: State) => void> = [] +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => listener(memoryState)) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + dispatch({ type: 'ADD_TOAST', toast: { ...props, id } }) + dispatch({ type: 'DISMISS_TOAST', toastId: id }) + + return { + id, + dismiss: () => dispatch({ type: 'DISMISS_TOAST', toastId: id }), + update: (props: Partial) => dispatch({ type: 'UPDATE_TOAST', toast: { ...props, id } }), + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) listeners.splice(index, 1) + } + }, []) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }), + } +} + +export { useToast, toast } diff --git a/views/js/admin/settings-app/src/lib/utils.ts b/views/js/admin/settings-app/src/lib/utils.ts new file mode 100644 index 000000000..fed2fe91e --- /dev/null +++ b/views/js/admin/settings-app/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/views/js/admin/settings-app/src/main.tsx b/views/js/admin/settings-app/src/main.tsx new file mode 100644 index 000000000..273674a88 --- /dev/null +++ b/views/js/admin/settings-app/src/main.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './globals.css' +import type { SaferpaySettingsData } from '@/types' +import { initTranslations } from '@/utils/translations' + +function parseSettingsData(): SaferpaySettingsData | null { + const el = document.getElementById('saferpay-settings-data') + if (!el?.textContent) return null + + try { + return JSON.parse(el.textContent) as SaferpaySettingsData + } catch { + return null + } +} + +document.addEventListener('DOMContentLoaded', () => { + const rootEl = document.getElementById('saferpay-settings-root') + if (!rootEl) return + + const data = parseSettingsData() + if (!data) { + rootEl.innerHTML = '
Failed to load settings data.
' + return + } + + window.saferpaySettingsData = data + initTranslations(data.translations || {}) + + ReactDOM.createRoot(rootEl).render( + + + , + ) +}) diff --git a/views/js/admin/settings-app/src/types/index.ts b/views/js/admin/settings-app/src/types/index.ts new file mode 100644 index 000000000..83233ba71 --- /dev/null +++ b/views/js/admin/settings-app/src/types/index.ts @@ -0,0 +1,80 @@ +export interface PaymentMethodData { + name: string + displayName: string + enabled: boolean + showLogos: boolean + showCustomForm: boolean + hasCustomForm: boolean + countries: number[] + currencies: number[] +} + +export interface TerminalOption { + id: string + name: string +} + +export interface SaferpaySettingsData { + // Environment + testMode: boolean + + // Test credentials + testUsername: string + testPassword: string + testTerminalId: string + testMerchantEmails: string + testFieldAccessToken: string + testFieldJsUrl: string + // Live credentials + liveUsername: string + livePassword: string + liveTerminalId: string + liveMerchantEmails: string + liveFieldAccessToken: string + liveFieldJsUrl: string + + // License (read-only, auto-detected from API, per environment) + testHasBusinessLicense: boolean + liveHasBusinessLicense: boolean + + // Payment Processing + paymentBehavior: number + paymentBehaviorWithout3D: number + restrictRefund: number + orderCreationAfterAuth: number + groupCards: boolean + groupCardsLogo: boolean + creditCardSave: number + + // Email + allowSaferpayMail: boolean + sendNewOrderMail: boolean + sendOrderConfMail: boolean + + // General + orderStateAwaitingPayment: number + paymentDescription: string + configurationName: string + orderIdOption: number + debugMode: boolean + + // Reference data + orderStates: Array<{ id: number; name: string }> + countries: Array<{ id: number; name: string }> + currencies: Array<{ id: number; iso_code: string }> + paymentMethods: PaymentMethodData[] + paymentMethodsFetchFailed?: boolean + + // Endpoints + ajaxUrl: string + adminToken: string + + // Translations + translations: Record +} + +declare global { + interface Window { + saferpaySettingsData: SaferpaySettingsData + } +} diff --git a/views/js/admin/settings-app/src/utils/translations.ts b/views/js/admin/settings-app/src/utils/translations.ts new file mode 100644 index 000000000..131018d00 --- /dev/null +++ b/views/js/admin/settings-app/src/utils/translations.ts @@ -0,0 +1,13 @@ +let translations: Record = {} + +export function initTranslations(t: Record) { + translations = t +} + +export function t(key: string, ...args: (string | number)[]): string { + let str = translations[key] || key + args.forEach((arg) => { + str = str.replace('%s', String(arg)) + }) + return str +} diff --git a/views/js/admin/settings-app/tailwind.config.ts b/views/js/admin/settings-app/tailwind.config.ts new file mode 100644 index 000000000..58d3b930c --- /dev/null +++ b/views/js/admin/settings-app/tailwind.config.ts @@ -0,0 +1,71 @@ +import type { Config } from 'tailwindcss' + +const config: Config = { + prefix: 'sp-', + important: '.sp-saferpay-root', + darkMode: ['class'], + content: ['./src/**/*.{ts,tsx}'], + corePlugins: { + preflight: false, + }, + theme: { + extend: { + colors: { + background: 'hsl(var(--sp-background))', + foreground: 'hsl(var(--sp-foreground))', + card: { + DEFAULT: 'hsl(var(--sp-card))', + foreground: 'hsl(var(--sp-card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--sp-popover))', + foreground: 'hsl(var(--sp-popover-foreground))', + }, + primary: { + DEFAULT: 'hsl(var(--sp-primary))', + foreground: 'hsl(var(--sp-primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--sp-secondary))', + foreground: 'hsl(var(--sp-secondary-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--sp-muted))', + foreground: 'hsl(var(--sp-muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--sp-accent))', + foreground: 'hsl(var(--sp-accent-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--sp-destructive))', + foreground: 'hsl(var(--sp-destructive-foreground))', + }, + border: 'hsl(var(--sp-border))', + input: 'hsl(var(--sp-input))', + ring: 'hsl(var(--sp-ring))', + }, + borderRadius: { + lg: 'var(--sp-radius)', + md: 'calc(var(--sp-radius) - 2px)', + sm: 'calc(var(--sp-radius) - 4px)', + }, + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, + }, + }, + plugins: [require('tailwindcss-animate')], +} +export default config diff --git a/views/js/admin/settings-app/tsconfig.json b/views/js/admin/settings-app/tsconfig.json new file mode 100644 index 000000000..22413dda4 --- /dev/null +++ b/views/js/admin/settings-app/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/*"] + }, + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/views/js/admin/settings-app/vite.config.ts b/views/js/admin/settings-app/vite.config.ts new file mode 100644 index 000000000..c44b8be17 --- /dev/null +++ b/views/js/admin/settings-app/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + plugins: [react()], + base: './', + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + build: { + outDir: '../dist', + emptyOutDir: true, + rollupOptions: { + output: { + entryFileNames: 'saferpay-settings.js', + assetFileNames: 'saferpay-settings.[ext]', + }, + }, + }, +}) diff --git a/views/js/front/hosted-templates/template1.js b/views/js/front/hosted-templates/template1.js deleted file mode 100644 index 1ec9ae6c4..000000000 --- a/views/js/front/hosted-templates/template1.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - *NOTICE OF LICENSE - * - *This source file is subject to the Open Software License (OSL 3.0) - *that is bundled with this package in the file LICENSE.txt. - *It is also available through the world-wide-web at this URL: - *http://opensource.org/licenses/osl-3.0.php - *If you did not receive a copy of the license and are unable to - *obtain it through the world-wide-web, please send an email - *to license@prestashop.com so we can send you a copy immediately. - * - *DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - *versions in the future. If you wish to customize PrestaShop for your - *needs please refer to http://www.prestashop.com for more information. - * - *@author INVERTUS UAB www.invertus.eu - *@copyright SIX Payment Services - *@license SIX Payment Services - */ - -var fields_to_validate = [ - "holdername", - "cardnumber", - "expiration", - "cvc" -]; - -$(document).ready(function () { - SaferpayFields.init({ - apiKey: saferpay_field_access_token, - url: saferpay_field_url, - placeholders: { - holdername: holder_name, - cardnumber: '0000 0000 0000 0000', - expiration: 'MM/YYYY', - cvc: '000' - }, - onSuccess: function () { - var element = document.getElementById('submit_hosted_field'); - element.removeAttribute('disabled'); - }, - onError: function (evt) { - $('.initialize-error-message').text(evt.message); - $('.initialize-error').show(); - }, - onValidated: function (e) { - $('.validation-error').show(); - - if (e.isValid) { - $('.error-' + e.fieldType).hide(); - } else { - $('.error-' + e.fieldType).show(); - } - - var invalidFields = fields_to_validate.filter(function (item) { - return $('.error-' + item).is(':visible') - }) - - if (invalidFields.length === 0) { - $('.validation-error').hide(); - } - } - }) -}); \ No newline at end of file diff --git a/views/js/front/hosted-templates/template2.js b/views/js/front/hosted-templates/template2.js deleted file mode 100644 index e5950597a..000000000 --- a/views/js/front/hosted-templates/template2.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - *NOTICE OF LICENSE - * - *This source file is subject to the Open Software License (OSL 3.0) - *that is bundled with this package in the file LICENSE.txt. - *It is also available through the world-wide-web at this URL: - *http://opensource.org/licenses/osl-3.0.php - *If you did not receive a copy of the license and are unable to - *obtain it through the world-wide-web, please send an email - *to license@prestashop.com so we can send you a copy immediately. - * - *DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - *versions in the future. If you wish to customize PrestaShop for your - *needs please refer to http://www.prestashop.com for more information. - * - *@author INVERTUS UAB www.invertus.eu - *@copyright SIX Payment Services - *@license SIX Payment Services - */ - -var fields_to_validate = [ - "holdername", - "cardnumber", - "expiration", - "cvc" -]; - -$(document).ready(function () { - SaferpayFields.init({ - apiKey: saferpay_field_access_token, - url: saferpay_field_url, - placeholders: { - holdername: holder_name, - cardnumber: '0000 0000 0000 0000', - expiration: 'MM/YYYY', - cvc: '000' - }, - onError: function (evt) { - $('.initialize-error-message').text(evt.message); - $('.initialize-error').show(); - }, - onSuccess: function () { - var element = document.getElementById('submit_hosted_field'); - element.removeAttribute('disabled'); - }, - onValidated: function (e) { - $('.validation-error').show(); - - if (e.isValid) { - $('.error-' + e.fieldType).hide(); - } else { - $('.error-' + e.fieldType).show(); - } - - var invalidFields = fields_to_validate.filter(function (item) { - return $('.error-' + item).is(':visible') - }) - - if (invalidFields.length === 0) { - $('.validation-error').hide(); - } - } - }); -}); \ No newline at end of file diff --git a/views/js/front/hosted-templates/template3.js b/views/js/front/hosted-templates/template3.js deleted file mode 100644 index 31c24d77c..000000000 --- a/views/js/front/hosted-templates/template3.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - *NOTICE OF LICENSE - * - *This source file is subject to the Open Software License (OSL 3.0) - *that is bundled with this package in the file LICENSE.txt. - *It is also available through the world-wide-web at this URL: - *http://opensource.org/licenses/osl-3.0.php - *If you did not receive a copy of the license and are unable to - *obtain it through the world-wide-web, please send an email - *to license@prestashop.com so we can send you a copy immediately. - * - *DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - *versions in the future. If you wish to customize PrestaShop for your - *needs please refer to http://www.prestashop.com for more information. - * - *@author INVERTUS UAB www.invertus.eu - *@copyright SIX Payment Services - *@license SIX Payment Services - */ - -var fields_to_validate = [ - "cardnumber", - "expiration", - "cvc" -]; - -$(document).ready(function () { - SaferpayFields.init({ - apiKey: saferpay_field_access_token, - url: saferpay_field_url, - placeholders: { - cardnumber: '0000 0000 0000 0000', - expiration: 'MM/YY', - cvc: '000' - }, - onError: function (evt) { - $('.initialize-error-message').text(evt.message); - $('.initialize-error').show(); - }, - onSuccess: function () { - var element = document.getElementById("submit_hosted_field"); - element.removeAttribute("disabled"); - }, - onFocus: function (e) { - var imageContainer = $('.image-container'); - var creditCardContainer = $('.credit-card-container'); - var creditCard = $('#credit-card'); - var frontElements = [ - 'cardnumber', - 'expiration' - ]; - - if (frontElements.includes(e.fieldType)) { - if (imageContainer.hasClass('rotate-to-back')) { - imageContainer.removeClass('rotate-to-back'); - - setTimeout(function () { - creditCardContainer.removeClass('rotate-element'); - creditCard.removeClass('cardnumber expiration cvc'); - creditCard.addClass(e.fieldType); - }, 300); - } else { - creditCardContainer.removeClass('rotate-element'); - creditCard.removeClass('cardnumber expiration cvc'); - creditCard.addClass(e.fieldType); - } - } else { - imageContainer.addClass('rotate-to-back'); - - setTimeout(function () { - creditCardContainer.addClass('rotate-element'); - creditCard.removeClass('cardnumber expiration cvc'); - creditCard.addClass(e.fieldType); - }, 300); - } - }, - onValidated: function (e) { - $('.validation-error').show(); - - if (e.isValid) { - $('.error-' + e.fieldType).hide(); - } else { - $('.error-' + e.fieldType).show(); - } - - var invalidFields = fields_to_validate.filter(function (item) { - return $('.error-' + item).is(':visible') - }) - - if (invalidFields.length === 0) { - $('.validation-error').hide(); - } - } - }); -}); \ No newline at end of file diff --git a/views/js/front/hosted-templates/template_submit.js b/views/js/front/hosted-templates/template_submit.js deleted file mode 100644 index 9e041cc2c..000000000 --- a/views/js/front/hosted-templates/template_submit.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - *NOTICE OF LICENSE - * - *This source file is subject to the Open Software License (OSL 3.0) - *that is bundled with this package in the file LICENSE.txt. - *It is also available through the world-wide-web at this URL: - *http://opensource.org/licenses/osl-3.0.php - *If you did not receive a copy of the license and are unable to - *obtain it through the world-wide-web, please send an email - *to license@prestashop.com so we can send you a copy immediately. - * - *DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - *versions in the future. If you wish to customize PrestaShop for your - *needs please refer to http://www.prestashop.com for more information. - * - *@author INVERTUS UAB www.invertus.eu - *@copyright SIX Payment Services - *@license SIX Payment Services - */ - -$(document).ready(function () { - document.getElementById('submit_hosted_field').onclick = function () { - if (!areAllFieldsValid) { - return; - } - - SaferpayFields.submit({ - onSuccess: function (evt) { - - $.ajax(saferpay_official_ajax_url, { - method: 'POST', - data: { - action: 'submitHostedFields', - paymentMethod: saved_card_method, - selectedCard: $("[name=saferpay_selected_card]").val(), - fieldToken: evt.token, - isBusinessLicence: isBusinessLicence, - ajax: 1 - }, - success: function (response) { - if (isJsonString(response)) { - window.location = $.parseJSON(response).url; - } else { - $('.internal-error').show(); - } - } - }); - - }, - onError: function (evt) { - showSubmissionError(evt.message); - } - }); - }; -}); - -function areAllFieldsValid() { - let invalidFields = fields_to_validate.filter(function (item) { - return $('.error-' + item).is(':visible') - }) - - return invalidFields.length === 0; -} - -function isJsonString(str) { - try { - JSON.parse(str); - } catch (e) { - return false; - } - - return true; -} - -function showSubmissionError(message) { - $('.submission-error-message').text(message); - $('.submission-error').show(); -} \ No newline at end of file diff --git a/views/js/front/inline-fields.js b/views/js/front/inline-fields.js new file mode 100644 index 000000000..aa4bdad8d --- /dev/null +++ b/views/js/front/inline-fields.js @@ -0,0 +1,405 @@ +/** + *NOTICE OF LICENSE + * + *This source file is subject to the Open Software License (OSL 3.0) + *that is bundled with this package in the file LICENSE.txt. + *It is also available through the world-wide-web at this URL: + *http://opensource.org/licenses/osl-3.0.php + *If you did not receive a copy of the license and are unable to + *obtain it through the world-wide-web, please send an email + *to license@prestashop.com so we can send you a copy immediately. + * + *DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade PrestaShop to newer + *versions in the future. If you wish to customize PrestaShop for your + *needs please refer to http://www.prestashop.com for more information. + * + *@author INVERTUS UAB www.invertus.eu + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +/** + * Renders the Saferpay Fields card form inline in the default PrestaShop checkout. + * + * The Saferpay Fields SDK binds to four fixed element IDs (fields-holder-name, + * fields-card-number, fields-expiration, fields-cvc), offers no way to scope to a + * container and no teardown. Moving an initialised field iframe in the DOM reloads it and + * breaks the binding. To show the form directly under whichever Saferpay card option is + * selected, we therefore render a fresh form (plain readonly-input placeholders) INTO the + * selected option's container and (re-)initialise the SDK on it. Selecting a different card + * option rebuilds the form there and re-initialises — re-init is supported as long as the + * placeholders are fresh DIV/SPAN/readonly-input elements, not the iframes of a prior init. + */ +(function () { + if (typeof saferpay_field_access_token === 'undefined' || !saferpay_field_access_token) { + return; + } + + var SLOT_ID = 'saferpay-inline-fields'; + var safeHolderName = typeof holder_name !== 'undefined' ? holder_name : 'Holder name'; + var safeInternalError = typeof saferpay_internal_error !== 'undefined' + ? saferpay_internal_error + : 'An error occurred, please try again.'; + + // The container id the form is currently rendered into, to avoid a redundant + // rebuild+re-init when the same option fires a spurious change event. + var renderedContainerId = null; + + // The loading state makes the fields inert, so it must not outlive an init callback that + // never arrives: a silently failed SDK init would otherwise leave a form nobody can type + // into. Generous on purpose: it is a last resort, not the normal path (init is well under + // a second), and lifting it early would show the fields before they are styled. + var LOADING_TIMEOUT = 10000; + var loadingTimeout = null; + + // The customer-entered card inputs live in cross-origin Saferpay iframes, so their + // validity is only known through the SDK's onValidated callback. It fires when a field + // loses focus; an untouched field never fires it. We therefore default every required + // field to invalid so an all-empty form is correctly blocked on submit. + var REQUIRED_FIELDS = ['holdername', 'cardnumber', 'expiration', 'cvc']; + var fieldValidity = {}; + + // The Saferpay Fields SDK binds to fixed element IDs. Map each field type to its element + // so we can highlight the matching form-group on validation. + var FIELD_ELEMENT_IDS = { + holdername: 'fields-holder-name', + cardnumber: 'fields-card-number', + expiration: 'fields-expiration', + cvc: 'fields-cvc' + }; + + function resetValidity() { + fieldValidity = {}; + REQUIRED_FIELDS.forEach(function (fieldType) { + fieldValidity[fieldType] = false; + }); + } + + // Toggle a state class on the form-group wrapping a given field's iframe. + function toggleFieldClass(fieldType, className, on) { + var elementId = FIELD_ELEMENT_IDS[fieldType]; + if (elementId) { + $('#' + elementId).closest('.form-group').toggleClass(className, on); + } + } + + function fieldLabel(fieldType) { + var labels = { + holdername: safeHolderName, + cardnumber: typeof saferpay_field_label_cardnumber !== 'undefined' ? saferpay_field_label_cardnumber : 'Card number', + expiration: typeof saferpay_field_label_expiration !== 'undefined' ? saferpay_field_label_expiration : 'Expiry date', + cvc: typeof saferpay_field_label_cvc !== 'undefined' ? saferpay_field_label_cvc : 'CVC' + }; + return labels[fieldType] || fieldType; + } + + // Each card field is a fieldset whose legend sits in a notch on the top border — + // the Saferpay payment-page outlined style. The legend/label is our own element + // (outside the cross-origin iframe), so it can be styled freely; the SDK replaces + // the placeholder inside with its iframe. The placeholder is a bare div (not a + // readonly input) so the theme's input styling and browser password-manager icons + // cannot flash while the SDK loads; its CSS height matches the iframe that replaces + // it, so the form does not shift when the fields initialise. + function fieldMarkup(elementId, label, extraClass) { + return '' + + '
' + + ' ' + label + '' + + '
' + + '
'; + } + + // The slot starts in the "loading" state: the field iframes are kept invisible and inert + // until the SDK reports successful initialisation, because each iframe first paints with + // the SDK's default input styling and only then applies our injected stylesheet, so + // showing it earlier flashes an unstyled square input inside the outlined field. The + // fieldsets render muted while it lasts (see saferpay_checkout.css), because a field that + // looks ready but silently drops the click reads as broken. + function fieldsFormMarkup() { + return '' + + '
' + + ' ' + + ' ' + + ' ' + + fieldMarkup('fields-holder-name', safeHolderName) + + fieldMarkup('fields-card-number', fieldLabel('cardnumber')) + + '
' + + '
' + + fieldMarkup('fields-expiration', fieldLabel('expiration')) + + '
' + + '
' + + fieldMarkup('fields-cvc', fieldLabel('cvc')) + + '
' + + '
' + + ' ' + + '
'; + } + + // A Saferpay card option that renders inline Fields (Custom Form ON). These carry the + // hidden saferpayPaymentType input set to the hosted_iframe value. + function isInlineFieldsOption($form) { + var type = $form.find('[name="saferpayPaymentType"]').val(); + return typeof saferpay_payment_types !== 'undefined' + && type === saferpay_payment_types.hosted_iframe; + } + + function selectedCardValue($form) { + var method = $form.find('[name="saved_card_method"]').val(); + return parseInt($form.find('[name="selectedCreditCard_' + method + '"]').val(), 10) || 0; + } + + function stopLoading() { + if (loadingTimeout) { + clearTimeout(loadingTimeout); + loadingTimeout = null; + } + $('#' + SLOT_ID).removeClass('saferpay-fields-loading'); + } + + function removeSlot() { + stopLoading(); + $('#' + SLOT_ID).remove(); + renderedContainerId = null; + } + + // Render a fresh Fields form into the selected option's container and initialise the SDK + // on it. Rebuilding fresh readonly-input placeholders each time keeps re-initialisation + // valid when the customer switches between card options. + function renderInto($container) { + var containerId = $container.attr('id'); + if (renderedContainerId === containerId && $('#' + SLOT_ID).length) { + return; + } + + removeSlot(); + resetValidity(); + + $container.append(fieldsFormMarkup()); + renderedContainerId = containerId; + loadingTimeout = setTimeout(stopLoading, LOADING_TIMEOUT); + + SaferpayFields.init({ + accessToken: saferpay_field_access_token, + url: saferpay_field_url, + // Visible labels sit in the field border notch (see fieldMarkup), so the inputs + // themselves stay placeholder-free like Saferpay's own payment page. + placeholders: { + holdername: ' ', + cardnumber: ' ', + expiration: ' ', + cvc: ' ' + }, + // The card inputs render inside cross-origin iframes; module CSS cannot reach + // them, only these rules do. They are passed inline (not via cssUrl) on purpose: + // a cssUrl stylesheet is fetched through Saferpay's server after the iframes + // render, briefly flashing the SDK's default input styling; the style object + // travels with the init config, so the default look never paints. + // + // The visible field outline and label are drawn OUTSIDE the iframe, on the + // fieldset/legend wrapping it (see saferpay_checkout.css), so the inner input + // stays borderless and transparent, with no horizontal padding (the fieldset + // provides it). The input sizes to its content via top/bottom padding — an + // explicit height/line-height fights the SDK's own iframe sizing and pushes the + // text off-centre. The :focus rule clears the SDK's default focus border/outline + // so the field does not shrink or gain a stray outline when active — focus + // feedback is shown on the fieldset outline instead. + style: { + '.form-control': 'box-sizing: border-box; width: 100%; margin: 0; padding: 4px 0 12px; border: none; outline: none; background: transparent; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; font-size: 17px; line-height: normal; color: #1f2426; caret-color: rgb(39, 119, 119);', + '.form-control:focus': 'border: none; outline: none; box-shadow: none;', + // The SDK keeps the CVC input disabled until the card number passes its + // CheckCard lookup. Without this rule the .form-control declarations above + // apply to it unchanged, so a disabled field is indistinguishable from an + // editable one: text cursor on hover, live caret colour, normal text colour. + // Customers click it, get no caret, and read the field as broken. Blink and + // WebKit override `color` on a disabled input, hence -webkit-text-fill-color. + '.form-control:disabled': 'cursor: not-allowed; color: #9aa4a8; -webkit-text-fill-color: #9aa4a8; caret-color: transparent;' + }, + // Reveal the field iframes only once the SDK reports them fully loaded (inner + // stylesheet included) — see the loading-state note on fieldsFormMarkup. + onSuccess: function () { + stopLoading(); + }, + onError: function (evt) { + stopLoading(); + $('#' + SLOT_ID + ' .initialize-error-message').text(evt.message); + $('#' + SLOT_ID + ' .initialize-error').show(); + }, + onValidated: function (evt) { + if (!evt || typeof evt.fieldType === 'undefined') { + return; + } + if (fieldValidity.hasOwnProperty(evt.fieldType)) { + fieldValidity[evt.fieldType] = !!evt.isValid; + } + toggleFieldClass(evt.fieldType, 'has-error', !evt.isValid); + + // The SDK disables the CVC input whenever the card number fails its CheckCard + // lookup, and exposes no callback for that. Card-number validity is the same + // condition it gates on, so it stands in as the lock signal. An untouched + // card number leaves the CVC enabled, which is why this only reacts to + // onValidated and the field is not rendered locked. + if (evt.fieldType === 'cardnumber') { + toggleFieldClass('cvc', 'is-locked', !evt.isValid); + } + }, + onFocus: function (evt) { + if (evt) { + toggleFieldClass(evt.fieldType, 'is-focused', true); + + // A disabled input cannot take focus, so reaching the CVC field proves + // the SDK has unlocked it. The card number may not have blurred yet, + // which is what onValidated waits for. + if (evt.fieldType === 'cvc') { + toggleFieldClass('cvc', 'is-locked', false); + } + } + }, + onBlur: function (evt) { + if (evt) { + toggleFieldClass(evt.fieldType, 'is-focused', false); + } + } + }); + } + + function showSubmissionError(message) { + $('#' + SLOT_ID + ' .submission-error-message').text(message); + $('#' + SLOT_ID + ' .submission-error').show(); + } + + function hideSubmissionError() { + $('#' + SLOT_ID + ' .submission-error').hide(); + } + + // PrestaShop's theme adds a "disabled" class to the place-order button when it is + // clicked (to guard against double submits). When we abort the submit — because a field + // is invalid or the SDK rejects it — nothing re-enables the button, so the customer is + // stuck. Restore it here so they can retry after fixing the fields. + function reEnablePlaceOrder() { + $('#payment-confirmation button[type="submit"]').removeClass('disabled').removeAttr('disabled'); + } + + // Returns the list of required fields that are not currently valid, marking each as + // invalid so the user sees which ones need attention. + function invalidFieldLabels() { + var labels = []; + REQUIRED_FIELDS.forEach(function (fieldType) { + if (!fieldValidity[fieldType]) { + labels.push(fieldLabel(fieldType)); + toggleFieldClass(fieldType, 'has-error', true); + } + }); + return labels; + } + + function incompleteFieldsMessage(labels) { + var prefix = typeof saferpay_fields_incomplete_error !== 'undefined' + ? saferpay_fields_incomplete_error + : 'Please check the following:'; + return prefix + ' ' + labels.join(', '); + } + + function submitFields($form) { + SaferpayFields.submit({ + onSuccess: function (evt) { + $.ajax(saferpay_official_ajax_url, { + method: 'POST', + data: { + action: 'submitHostedFields', + paymentMethod: $form.find('[name="saved_card_method"]').val(), + selectedCard: 0, + fieldToken: evt.token, + isBusinessLicence: 1, + ajax: 1 + }, + success: function (response) { + try { + // jQuery may already have parsed a JSON response into an object. + var data = typeof response === 'string' ? JSON.parse(response) : response; + if (data && data.url) { + window.location = data.url; + } else { + $('#' + SLOT_ID + ' .internal-error').show(); + reEnablePlaceOrder(); + } + } catch (e) { + $('#' + SLOT_ID + ' .internal-error').show(); + reEnablePlaceOrder(); + } + }, + error: function () { + $('#' + SLOT_ID + ' .internal-error').show(); + reEnablePlaceOrder(); + } + }); + }, + onError: function (evt) { + // The SDK's raw message (e.g. "cannot store data, because fields contains + // invalid or missing data") is not actionable. Surface the same clear, + // field-specific message we use for pre-submit gating instead. + if (evt && evt.message) { + console.warn('Saferpay Fields submit error: ' + evt.message); + } + var labels = invalidFieldLabels(); + showSubmissionError(labels.length ? incompleteFieldsMessage(labels) : safeInternalError); + reEnablePlaceOrder(); + } + }); + } + + $(document).ready(function () { + if (!$('[name="saferpayPaymentType"]').length) { + return; + } + + // Render / remove the form as payment options are selected. Rendering into the + // selected option's own container places the fields directly under it. + $('body').on('change', 'input[name="payment-option"]', function () { + var $option = $('#pay-with-' + $(this).attr('id') + '-form'); + var $form = $option.find('form').first(); + + if ($form.length && isInlineFieldsOption($form) && selectedCardValue($form) <= 0) { + renderInto($option); + } else { + removeSlot(); + } + }); + + // Handle a payment option that is already selected on load (e.g. single option or + // themes that pre-select) — the change event would not fire on its own. + $('input[name="payment-option"]:checked').trigger('change'); + + // Intercept the place-order submit for inline Fields (new card) options. + $('body').on('submit', '[id^=pay-with-][id$=-form] form', function (event) { + var $form = $(this); + + if (!isInlineFieldsOption($form) || selectedCardValue($form) > 0) { + return; // saved-card & non-Fields flows are handled elsewhere + } + + event.preventDefault(); + event.stopImmediatePropagation(); + + hideSubmissionError(); + + // Block submission while any required card field is empty/invalid and tell the + // customer exactly which ones, instead of letting the SDK fail with a cryptic + // "cannot store data" message. + var labels = invalidFieldLabels(); + if (labels.length) { + showSubmissionError(incompleteFieldsMessage(labels)); + reEnablePlaceOrder(); + return; + } + + submitFields($form); + }); + }); +})(); diff --git a/views/js/front/saferpay_iframe.js b/views/js/front/saferpay_iframe.js deleted file mode 100644 index 24e07c6e2..000000000 --- a/views/js/front/saferpay_iframe.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - *NOTICE OF LICENSE - * - *This source file is subject to the Open Software License (OSL 3.0) - *that is bundled with this package in the file LICENSE.txt. - *It is also available through the world-wide-web at this URL: - *http://opensource.org/licenses/osl-3.0.php - *If you did not receive a copy of the license and are unable to - *obtain it through the world-wide-web, please send an email - *to license@prestashop.com so we can send you a copy immediately. - * - *DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - *versions in the future. If you wish to customize PrestaShop for your - *needs please refer to http://www.prestashop.com for more information. - * - *@author INVERTUS UAB www.invertus.eu - *@copyright SIX Payment Services - *@license SIX Payment Services - */ - -(function () { - if (top.location != location) { - top.location.href = redirectUrl; - } -})(); \ No newline at end of file diff --git a/views/templates/admin/field-option-settings/helpers/index.php b/views/templates/admin/field-option-settings/helpers/index.php deleted file mode 100644 index ee6227264..000000000 --- a/views/templates/admin/field-option-settings/helpers/index.php +++ /dev/null @@ -1,31 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); - -header('Location: ../'); -exit; diff --git a/views/templates/admin/field-option-settings/helpers/options/index.php b/views/templates/admin/field-option-settings/helpers/options/index.php deleted file mode 100644 index ee6227264..000000000 --- a/views/templates/admin/field-option-settings/helpers/options/index.php +++ /dev/null @@ -1,31 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); - -header('Location: ../'); -exit; diff --git a/views/templates/admin/field-option-settings/helpers/options/options.tpl b/views/templates/admin/field-option-settings/helpers/options/options.tpl deleted file mode 100644 index a51d8345e..000000000 --- a/views/templates/admin/field-option-settings/helpers/options/options.tpl +++ /dev/null @@ -1,76 +0,0 @@ -{** - *NOTICE OF LICENSE - * - *This source file is subject to the Open Software License (OSL 3.0) - *that is bundled with this package in the file LICENSE.txt. - *It is also available through the world-wide-web at this URL: - *http://opensource.org/licenses/osl-3.0.php - *If you did not receive a copy of the license and are unable to - *obtain it through the world-wide-web, please send an email - *to license@prestashop.com so we can send you a copy immediately. - * - *DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - *versions in the future. If you wish to customize PrestaShop for your - *needs please refer to http://www.prestashop.com for more information. - * - *@author INVERTUS UAB www.invertus.eu - *@copyright SIX Payment Services - *@license SIX Payment Services - *} - -{extends file="helpers/options/options.tpl"} - -{block name="input" append} - {if $field['type'] == 'password_input'} -
- -
- {/if} - {if $field['type'] == 'desc'} -
- {if $field['template'] == 'field-javascript-library-desc.tpl'} - {include file="../../../partials/field-javascript-library-desc.tpl"} - {/if} - - {if $field['template'] == 'field-access-token-desc.tpl'} - {include file="../../../partials/field-access-token-desc.tpl"} - {/if} - - {if $field['template'] == 'field-hosted-field-template-desc.tpl'} - {include file="../../../partials/field-hosted-field-template-desc.tpl"} - {/if} - {if $field['template'] == 'field-new-order-mail-desc.tpl'} - {include file="../../../partials/field-new-order-mail-desc.tpl"} - {/if} -
- {/if} - - {if $field['type'] == 'select-template'} - -
- {foreach from=$field['templateOptions'] key=key item=templateUrl} - {assign var='key' value=$key + 1} {* To have normal keys without 0 *} - - {/foreach} -
- - {/if} - - {if $field['type'] == 'terminal_selector'} -
- {include file="../../../partials/field-terminal-id.tpl"} -
- {/if} -{/block} diff --git a/views/templates/admin/field-option-settings/index.php b/views/templates/admin/field-option-settings/index.php deleted file mode 100644 index ee6227264..000000000 --- a/views/templates/admin/field-option-settings/index.php +++ /dev/null @@ -1,31 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); - -header('Location: ../'); -exit; diff --git a/views/templates/admin/logs/log_modal.tpl b/views/templates/admin/logs/log_modal.tpl index 62cb43dd4..eadfc7fcd 100644 --- a/views/templates/admin/logs/log_modal.tpl +++ b/views/templates/admin/logs/log_modal.tpl @@ -19,7 +19,8 @@ *@copyright SIX Payment Services *@license SIX Payment Services *} -
{l s='View' mod='saferpayofficial'} -
+ -