Merge branch 'trunk' into PCP-2347-new-feature-accelerated-checkout

# Conflicts:
#	modules.php
This commit is contained in:
Pedro Silva 2024-04-12 10:22:47 +01:00
commit 9cb63040d8
No known key found for this signature in database
GPG key ID: E2EE20C0669D24B3
129 changed files with 7018 additions and 1124 deletions

View file

@ -1,6 +1,6 @@
name: e2e tests name: e2e tests
on: [push] on: workflow_dispatch
jobs: jobs:
build: build:

View file

@ -87,6 +87,32 @@ function ppcp_capture_order( WC_Order $wc_order ): void {
} }
} }
/**
* Reauthorizes the PayPal order.
*
* @param WC_Order $wc_order The WC order.
* @throws InvalidArgumentException When the order cannot be captured.
* @throws Exception When the operation fails.
*/
function ppcp_reauthorize_order( WC_Order $wc_order ): void {
$intent = strtoupper( (string) $wc_order->get_meta( PayPalGateway::INTENT_META_KEY ) );
if ( $intent !== 'AUTHORIZE' ) {
throw new InvalidArgumentException( 'Only orders with "authorize" intent can be reauthorized.' );
}
$captured = wc_string_to_bool( $wc_order->get_meta( AuthorizedPaymentsProcessor::CAPTURED_META_KEY ) );
if ( $captured ) {
throw new InvalidArgumentException( 'The order is already captured.' );
}
$authorized_payment_processor = PPCP::container()->get( 'wcgateway.processor.authorized-payments' );
assert( $authorized_payment_processor instanceof AuthorizedPaymentsProcessor );
if ( $authorized_payment_processor->reauthorize_payment( $wc_order ) !== AuthorizedPaymentsProcessor::SUCCESSFUL ) {
throw new RuntimeException( $authorized_payment_processor->reauthorization_failure_reason() ?: 'Reauthorization failed.' );
}
}
/** /**
* Refunds the PayPal order. * Refunds the PayPal order.
* Note that you can use wc_refund_payment() to trigger the refund in WC and PayPal. * Note that you can use wc_refund_payment() to trigger the refund in WC and PayPal.

View file

@ -1,5 +1,54 @@
*** Changelog *** *** Changelog ***
= 2.6.1 - 2024-04-09 =
* Fix - Payment tokens fixes and adjustments #2106
* Fix - Pay upon Invoice: Add input validation to Experience Context fields #2092
* Fix - Disable markup in get_plugin_data() returns to fix an issue with wptexturize() #2094
* Fix - Problem changing the shipping option in block pages #2142
* Fix - Saved payment token deleted after payment with another saved payment token #2146
* Enhancement - Pay later messaging configurator improvements #2107
* Enhancement - Replace the middleware URL from connect.woocommerce.com to api.woocommerce.com/integrations #2130
* Enhancement - Remove all Sofort references as it has been deprecated #2124
* Enhancement - Improve funding source names #2118
* Enhancement - More fraud prevention capabilities by storing additional data in the order #2125
* Enhancement - Update ACDC currency eligibility for AMEX #2129
* Enhancement - Sync shipping options with Venmo when skipping final confirmation on Checkout #2108
* Enhancement - Card Fields: Add a filter for the CVC field and update the placeholder to match the label #2089
* Enhancement - Product Title: Sanitize before sending to PayPal #2090
* Enhancement - Add filter for disabling permit_multiple_payment_tokens vault attribute #2136
* Enhancement - Filter to hide PayPal email address not working on order detail #2137
= 2.6.0 - 2024-03-20 =
* Fix - invoice_id not included in API call when creating payment with saved card #2086
* Fix - Typo in SCA indicators for ACDC Vault transactions #2083
* Fix - Payments with saved card tokens use Capture intent when Authorize is configured #2069
* Fix - WooPayments multi-currency causing currency mismatch error on Block Cart & Checkout pages #2054
* Fix - "Must pass createSubscription with intent=subscription" error with PayPal Subscriptions mode #2058
* Fix - "Proceed to PayPal" button displayed for Free trial PayPal Subscription products when payment token is saved #2041
* Fix - ACDC payments with new credit card may fail when debugging is enabled (JSON malformed by warning) #2051
* Enhancement - Add Pay Later Messaging block #1897
* Enhancement - Submit the form instead of refreshing the page to show the save notice #2081
* Enhancement - Integrate pay later messaging block with the messaging configurator #2080
* Enhancement - Reauthorize authorized payments #2062
* Enhancement - Do not handle VAULT.PAYMENT-TOKEN.CREATED webhook for Vault v3 #2079
* Enhancement - Improve the messaging configurator styles #2053
* Enhancement - Ensure PayPal Vaulting is not selected as Subscriptions Mode when Reference Transactions are disabled #2057
* Enhancement - Pay later messaging configurator & messaging block adjustments #2096
= 2.5.4 - 2024-02-27 =
* Fix - Cannot enable Apple Pay when API credentials were manually created #2015
* Fix - Cart simulation type error #1943
* Enhancement - Apple Pay recurring payments #1986
* Enhancement - Real Time Account Updater (RTAU) integration #2027
* Enhancement - Prepare the SKU for sending to PayPal #2033
* Enhancement - Store the Card Brand in Address Verification Result instead of 3DS authentication result #2026
* Enhancement - Update country eligibility for AdvancedCard Processing, Apple Pay, Google Pay #2019
* Enhancement - Disable PayPal Vaulting setting instead of hiding it when Reference Transactions not available #2029
* Enhancement - Store three d secure enrollment status and authentication status responses in wc order #1980
* Enhancement - Add more checks to prevent "PayPal order ID not found" errors #2038
* Enhancement - Disable messaging configurator when vault is enabled #2042
* Feature preview - Pay Later Messaging configurator #1924
= 2.5.3 - 2024-02-06 = = 2.5.3 - 2024-02-06 =
* Fix - Free trial subscription products using PayPal Vaulting when PayPal Subscriptions configured as Subscriptions Mode #1979 * Fix - Free trial subscription products using PayPal Vaulting when PayPal Subscriptions configured as Subscriptions Mode #1979
* Fix - Pay by link - Germany - PayPal buttons are not visible on Pay for order page #2014 * Fix - Pay by link - Germany - PayPal buttons are not visible on Pay for order page #2014

350
composer.lock generated
View file

@ -8,20 +8,20 @@
"packages": [ "packages": [
{ {
"name": "container-interop/service-provider", "name": "container-interop/service-provider",
"version": "v0.4.0", "version": "v0.4.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/container-interop/service-provider.git", "url": "https://github.com/container-interop/service-provider.git",
"reference": "4969b9e49460690b7430b3f1a87cab07be61418a" "reference": "e04441ca21ef03e10dce70b0af29269281eec6dc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/container-interop/service-provider/zipball/4969b9e49460690b7430b3f1a87cab07be61418a", "url": "https://api.github.com/repos/container-interop/service-provider/zipball/e04441ca21ef03e10dce70b0af29269281eec6dc",
"reference": "4969b9e49460690b7430b3f1a87cab07be61418a", "reference": "e04441ca21ef03e10dce70b0af29269281eec6dc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"psr/container": "^1.0" "psr/container": "^1.0 || ^2.0"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@ -37,9 +37,9 @@
"homepage": "https://github.com/container-interop/service-provider", "homepage": "https://github.com/container-interop/service-provider",
"support": { "support": {
"issues": "https://github.com/container-interop/service-provider/issues", "issues": "https://github.com/container-interop/service-provider/issues",
"source": "https://github.com/container-interop/service-provider/tree/master" "source": "https://github.com/container-interop/service-provider/tree/v0.4.1"
}, },
"time": "2017-09-20T14:13:36+00:00" "time": "2023-12-14T14:50:12+00:00"
}, },
{ {
"name": "dhii/human-readable-interface", "name": "dhii/human-readable-interface",
@ -438,16 +438,16 @@
}, },
{ {
"name": "symfony/polyfill-php80", "name": "symfony/polyfill-php80",
"version": "v1.27.0", "version": "v1.29.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php80.git", "url": "https://github.com/symfony/polyfill-php80.git",
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -455,9 +455,6 @@
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "url": "https://github.com/symfony/polyfill"
@ -501,7 +498,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
}, },
"funding": [ "funding": [
{ {
@ -517,7 +514,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2024-01-29T20:11:03+00:00"
}, },
{ {
"name": "wikimedia/composer-merge-plugin", "name": "wikimedia/composer-merge-plugin",
@ -797,16 +794,16 @@
}, },
{ {
"name": "antecedent/patchwork", "name": "antecedent/patchwork",
"version": "2.1.25", "version": "2.1.28",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/antecedent/patchwork.git", "url": "https://github.com/antecedent/patchwork.git",
"reference": "17314e042d45e0dacb0a494c2d1ef50e7621136a" "reference": "6b30aff81ebadf0f2feb9268d3e08385cebcc08d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/antecedent/patchwork/zipball/17314e042d45e0dacb0a494c2d1ef50e7621136a", "url": "https://api.github.com/repos/antecedent/patchwork/zipball/6b30aff81ebadf0f2feb9268d3e08385cebcc08d",
"reference": "17314e042d45e0dacb0a494c2d1ef50e7621136a", "reference": "6b30aff81ebadf0f2feb9268d3e08385cebcc08d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -827,7 +824,7 @@
} }
], ],
"description": "Method redefinition (monkey-patching) functionality for PHP.", "description": "Method redefinition (monkey-patching) functionality for PHP.",
"homepage": "http://patchwork2.org/", "homepage": "https://antecedent.github.io/patchwork/",
"keywords": [ "keywords": [
"aop", "aop",
"aspect", "aspect",
@ -839,9 +836,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/antecedent/patchwork/issues", "issues": "https://github.com/antecedent/patchwork/issues",
"source": "https://github.com/antecedent/patchwork/tree/2.1.25" "source": "https://github.com/antecedent/patchwork/tree/2.1.28"
}, },
"time": "2023-02-19T12:51:24+00:00" "time": "2024-02-06T09:26:11+00:00"
}, },
{ {
"name": "brain/monkey", "name": "brain/monkey",
@ -988,16 +985,16 @@
}, },
{ {
"name": "composer/pcre", "name": "composer/pcre",
"version": "2.1.0", "version": "2.1.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/composer/pcre.git", "url": "https://github.com/composer/pcre.git",
"reference": "3fdb2807b31a78a40ad89570e30ec77466c98717" "reference": "b439557066cd445732fa57cbc8d905394b4db8a0"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/3fdb2807b31a78a40ad89570e30ec77466c98717", "url": "https://api.github.com/repos/composer/pcre/zipball/b439557066cd445732fa57cbc8d905394b4db8a0",
"reference": "3fdb2807b31a78a40ad89570e30ec77466c98717", "reference": "b439557066cd445732fa57cbc8d905394b4db8a0",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1039,7 +1036,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/composer/pcre/issues", "issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/2.1.0" "source": "https://github.com/composer/pcre/tree/2.1.1"
}, },
"funding": [ "funding": [
{ {
@ -1055,20 +1052,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-16T18:32:04+00:00" "time": "2023-10-11T07:10:55+00:00"
}, },
{ {
"name": "composer/semver", "name": "composer/semver",
"version": "3.3.2", "version": "3.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/composer/semver.git", "url": "https://github.com/composer/semver.git",
"reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32",
"reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1118,9 +1115,9 @@
"versioning" "versioning"
], ],
"support": { "support": {
"irc": "irc://irc.freenode.org/composer", "irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues", "issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.3.2" "source": "https://github.com/composer/semver/tree/3.4.0"
}, },
"funding": [ "funding": [
{ {
@ -1136,7 +1133,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-04-01T19:23:25+00:00" "time": "2023-08-31T09:50:34+00:00"
}, },
{ {
"name": "composer/xdebug-handler", "name": "composer/xdebug-handler",
@ -1604,24 +1601,24 @@
}, },
{ {
"name": "graham-campbell/result-type", "name": "graham-campbell/result-type",
"version": "v1.1.1", "version": "v1.1.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git", "url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831" "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/fbd48bce38f73f8a4ec8583362e732e4095e5862",
"reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2.5 || ^8.0", "php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.1" "phpoption/phpoption": "^1.9.2"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@ -1650,7 +1647,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues", "issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1" "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.2"
}, },
"funding": [ "funding": [
{ {
@ -1662,7 +1659,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-02-25T20:23:15+00:00" "time": "2023-11-12T22:16:48+00:00"
}, },
{ {
"name": "hamcrest/hamcrest-php", "name": "hamcrest/hamcrest-php",
@ -1845,16 +1842,16 @@
}, },
{ {
"name": "netresearch/jsonmapper", "name": "netresearch/jsonmapper",
"version": "v4.2.0", "version": "v4.4.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/cweiske/jsonmapper.git", "url": "https://github.com/cweiske/jsonmapper.git",
"reference": "f60565f8c0566a31acf06884cdaa591867ecc956" "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/f60565f8c0566a31acf06884cdaa591867ecc956", "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0",
"reference": "f60565f8c0566a31acf06884cdaa591867ecc956", "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1865,7 +1862,7 @@
"php": ">=7.1" "php": ">=7.1"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "~7.5 || ~8.0 || ~9.0", "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0",
"squizlabs/php_codesniffer": "~3.5" "squizlabs/php_codesniffer": "~3.5"
}, },
"type": "library", "type": "library",
@ -1890,22 +1887,22 @@
"support": { "support": {
"email": "cweiske@cweiske.de", "email": "cweiske@cweiske.de",
"issues": "https://github.com/cweiske/jsonmapper/issues", "issues": "https://github.com/cweiske/jsonmapper/issues",
"source": "https://github.com/cweiske/jsonmapper/tree/v4.2.0" "source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1"
}, },
"time": "2023-04-09T17:37:40+00:00" "time": "2024-01-31T06:18:54+00:00"
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v4.16.0", "version": "v4.18.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/PHP-Parser.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "19526a33fb561ef417e822e85f08a00db4059c17" "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999",
"reference": "19526a33fb561ef417e822e85f08a00db4059c17", "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1946,9 +1943,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nikic/PHP-Parser/issues", "issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0"
}, },
"time": "2023-06-25T14:52:30+00:00" "time": "2023-12-10T21:03:43+00:00"
}, },
{ {
"name": "openlss/lib-array2xml", "name": "openlss/lib-array2xml",
@ -2539,16 +2536,16 @@
}, },
{ {
"name": "phpoption/phpoption", "name": "phpoption/phpoption",
"version": "1.9.1", "version": "1.9.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/schmittjoh/php-option.git", "url": "https://github.com/schmittjoh/php-option.git",
"reference": "dd3a383e599f49777d8b628dadbb90cae435b87e" "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e", "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/80735db690fe4fc5c76dfa7f9b770634285fa820",
"reference": "dd3a383e599f49777d8b628dadbb90cae435b87e", "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2556,7 +2553,7 @@
}, },
"require-dev": { "require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2", "bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -2598,7 +2595,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/schmittjoh/php-option/issues", "issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.1" "source": "https://github.com/schmittjoh/php-option/tree/1.9.2"
}, },
"funding": [ "funding": [
{ {
@ -2610,7 +2607,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-02-25T19:38:58+00:00" "time": "2023-11-12T21:59:55+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
@ -2911,16 +2908,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "8.5.33", "version": "8.5.36",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e" "reference": "9652df58e06a681429d8cfdaec3c43d6de581d5a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9652df58e06a681429d8cfdaec3c43d6de581d5a",
"reference": "7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e", "reference": "9652df58e06a681429d8cfdaec3c43d6de581d5a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2950,9 +2947,9 @@
"sebastian/version": "^2.0.1" "sebastian/version": "^2.0.1"
}, },
"suggest": { "suggest": {
"ext-soap": "*", "ext-soap": "To be able to generate mocks based on WSDL files",
"ext-xdebug": "*", "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage",
"phpunit/php-invoker": "^2.0.0" "phpunit/php-invoker": "To allow enforcing time limits"
}, },
"bin": [ "bin": [
"phpunit" "phpunit"
@ -2988,7 +2985,8 @@
], ],
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.33" "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.36"
}, },
"funding": [ "funding": [
{ {
@ -3004,7 +3002,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-02-27T13:04:50+00:00" "time": "2023-12-01T16:52:15+00:00"
}, },
{ {
"name": "sebastian/code-unit-reverse-lookup", "name": "sebastian/code-unit-reverse-lookup",
@ -3343,16 +3341,16 @@
}, },
{ {
"name": "sebastian/global-state", "name": "sebastian/global-state",
"version": "3.0.2", "version": "3.0.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git", "url": "https://github.com/sebastianbergmann/global-state.git",
"reference": "de036ec91d55d2a9e0db2ba975b512cdb1c23921" "reference": "66783ce213de415b451b904bfef9dda0cf9aeae0"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/de036ec91d55d2a9e0db2ba975b512cdb1c23921", "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/66783ce213de415b451b904bfef9dda0cf9aeae0",
"reference": "de036ec91d55d2a9e0db2ba975b512cdb1c23921", "reference": "66783ce213de415b451b904bfef9dda0cf9aeae0",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3395,7 +3393,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/global-state/issues", "issues": "https://github.com/sebastianbergmann/global-state/issues",
"source": "https://github.com/sebastianbergmann/global-state/tree/3.0.2" "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.3"
}, },
"funding": [ "funding": [
{ {
@ -3403,7 +3401,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2022-02-10T06:55:38+00:00" "time": "2023-08-02T09:23:32+00:00"
}, },
{ {
"name": "sebastian/object-enumerator", "name": "sebastian/object-enumerator",
@ -3737,16 +3735,16 @@
}, },
{ {
"name": "squizlabs/php_codesniffer", "name": "squizlabs/php_codesniffer",
"version": "3.7.2", "version": "3.8.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" "reference": "14f5fff1e64118595db5408e946f3a22c75807f7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/14f5fff1e64118595db5408e946f3a22c75807f7",
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", "reference": "14f5fff1e64118595db5408e946f3a22c75807f7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3756,11 +3754,11 @@
"php": ">=5.4.0" "php": ">=5.4.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
}, },
"bin": [ "bin": [
"bin/phpcs", "bin/phpcbf",
"bin/phpcbf" "bin/phpcs"
], ],
"type": "library", "type": "library",
"extra": { "extra": {
@ -3775,35 +3773,58 @@
"authors": [ "authors": [
{ {
"name": "Greg Sherwood", "name": "Greg Sherwood",
"role": "lead" "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.", "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
"homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
"keywords": [ "keywords": [
"phpcs", "phpcs",
"standards", "standards",
"static analysis" "static analysis"
], ],
"support": { "support": {
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
"source": "https://github.com/squizlabs/PHP_CodeSniffer", "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
"wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
}, },
"time": "2023-02-22T23:07:41+00:00" "funding": [
{
"url": "https://github.com/PHPCSStandards",
"type": "github"
},
{
"url": "https://github.com/jrfnl",
"type": "github"
},
{
"url": "https://opencollective.com/php_codesniffer",
"type": "open_collective"
}
],
"time": "2024-01-11T20:47:48+00:00"
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v5.4.26", "version": "v5.4.35",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "b504a3d266ad2bb632f196c0936ef2af5ff6e273" "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/b504a3d266ad2bb632f196c0936ef2af5ff6e273", "url": "https://api.github.com/repos/symfony/console/zipball/dbdf6adcb88d5f83790e1efb57ef4074309d3931",
"reference": "b504a3d266ad2bb632f196c0936ef2af5ff6e273", "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3873,7 +3894,7 @@
"terminal" "terminal"
], ],
"support": { "support": {
"source": "https://github.com/symfony/console/tree/v5.4.26" "source": "https://github.com/symfony/console/tree/v5.4.35"
}, },
"funding": [ "funding": [
{ {
@ -3889,7 +3910,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-07-19T20:11:33+00:00" "time": "2024-01-23T14:28:09+00:00"
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
@ -3960,16 +3981,16 @@
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.27.0", "version": "v1.29.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a" "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a", "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3983,9 +4004,6 @@
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "url": "https://github.com/symfony/polyfill"
@ -4022,7 +4040,7 @@
"portable" "portable"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
}, },
"funding": [ "funding": [
{ {
@ -4038,20 +4056,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2024-01-29T20:11:03+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-grapheme", "name": "symfony/polyfill-intl-grapheme",
"version": "v1.27.0", "version": "v1.29.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "511a08c03c1960e08a883f4cffcacd219b758354" "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f",
"reference": "511a08c03c1960e08a883f4cffcacd219b758354", "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4062,9 +4080,6 @@
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "url": "https://github.com/symfony/polyfill"
@ -4103,7 +4118,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0"
}, },
"funding": [ "funding": [
{ {
@ -4119,20 +4134,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2024-01-29T20:11:03+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
"version": "v1.27.0", "version": "v1.29.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" "reference": "bc45c394692b948b4d383a08d7753968bed9a83d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d",
"reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", "reference": "bc45c394692b948b4d383a08d7753968bed9a83d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4143,9 +4158,6 @@
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "url": "https://github.com/symfony/polyfill"
@ -4187,7 +4199,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0"
}, },
"funding": [ "funding": [
{ {
@ -4203,20 +4215,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2024-01-29T20:11:03+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.27.0", "version": "v1.29.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4230,9 +4242,6 @@
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "url": "https://github.com/symfony/polyfill"
@ -4270,7 +4279,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
}, },
"funding": [ "funding": [
{ {
@ -4286,20 +4295,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2024-01-29T20:11:03+00:00"
}, },
{ {
"name": "symfony/polyfill-php73", "name": "symfony/polyfill-php73",
"version": "v1.27.0", "version": "v1.29.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php73.git", "url": "https://github.com/symfony/polyfill-php73.git",
"reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9" "reference": "21bd091060673a1177ae842c0ef8fe30893114d2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9", "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2",
"reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9", "reference": "21bd091060673a1177ae842c0ef8fe30893114d2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4307,9 +4316,6 @@
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "url": "https://github.com/symfony/polyfill"
@ -4349,7 +4355,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php73/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0"
}, },
"funding": [ "funding": [
{ {
@ -4365,7 +4371,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2024-01-29T20:11:03+00:00"
}, },
{ {
"name": "symfony/service-contracts", "name": "symfony/service-contracts",
@ -4452,16 +4458,16 @@
}, },
{ {
"name": "symfony/string", "name": "symfony/string",
"version": "v5.4.26", "version": "v5.4.35",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/string.git", "url": "https://github.com/symfony/string.git",
"reference": "1181fe9270e373537475e826873b5867b863883c" "reference": "c209c4d0559acce1c9a2067612cfb5d35756edc2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/1181fe9270e373537475e826873b5867b863883c", "url": "https://api.github.com/repos/symfony/string/zipball/c209c4d0559acce1c9a2067612cfb5d35756edc2",
"reference": "1181fe9270e373537475e826873b5867b863883c", "reference": "c209c4d0559acce1c9a2067612cfb5d35756edc2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4518,7 +4524,7 @@
"utf8" "utf8"
], ],
"support": { "support": {
"source": "https://github.com/symfony/string/tree/v5.4.26" "source": "https://github.com/symfony/string/tree/v5.4.35"
}, },
"funding": [ "funding": [
{ {
@ -4534,20 +4540,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-06-28T12:46:07+00:00" "time": "2024-01-23T13:51:25+00:00"
}, },
{ {
"name": "theseer/tokenizer", "name": "theseer/tokenizer",
"version": "1.2.1", "version": "1.2.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/theseer/tokenizer.git", "url": "https://github.com/theseer/tokenizer.git",
"reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96",
"reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4576,7 +4582,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": { "support": {
"issues": "https://github.com/theseer/tokenizer/issues", "issues": "https://github.com/theseer/tokenizer/issues",
"source": "https://github.com/theseer/tokenizer/tree/1.2.1" "source": "https://github.com/theseer/tokenizer/tree/1.2.2"
}, },
"funding": [ "funding": [
{ {
@ -4584,7 +4590,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2021-07-28T10:34:58+00:00" "time": "2023-11-20T00:12:19+00:00"
}, },
{ {
"name": "vimeo/psalm", "name": "vimeo/psalm",
@ -4696,31 +4702,31 @@
}, },
{ {
"name": "vlucas/phpdotenv", "name": "vlucas/phpdotenv",
"version": "v5.5.0", "version": "v5.6.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/vlucas/phpdotenv.git", "url": "https://github.com/vlucas/phpdotenv.git",
"reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7" "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4",
"reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-pcre": "*", "ext-pcre": "*",
"graham-campbell/result-type": "^1.0.2", "graham-campbell/result-type": "^1.1.2",
"php": "^7.1.3 || ^8.0", "php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.8", "phpoption/phpoption": "^1.9.2",
"symfony/polyfill-ctype": "^1.23", "symfony/polyfill-ctype": "^1.24",
"symfony/polyfill-mbstring": "^1.23.1", "symfony/polyfill-mbstring": "^1.24",
"symfony/polyfill-php80": "^1.23.1" "symfony/polyfill-php80": "^1.24"
}, },
"require-dev": { "require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1", "bamarni/composer-bin-plugin": "^1.8.2",
"ext-filter": "*", "ext-filter": "*",
"phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25" "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
}, },
"suggest": { "suggest": {
"ext-filter": "Required to use the boolean validator." "ext-filter": "Required to use the boolean validator."
@ -4732,7 +4738,7 @@
"forward-command": true "forward-command": true
}, },
"branch-alias": { "branch-alias": {
"dev-master": "5.5-dev" "dev-master": "5.6-dev"
} }
}, },
"autoload": { "autoload": {
@ -4764,7 +4770,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/vlucas/phpdotenv/issues", "issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0" "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.0"
}, },
"funding": [ "funding": [
{ {
@ -4776,7 +4782,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-10-16T01:01:54+00:00" "time": "2023-11-12T22:43:29+00:00"
}, },
{ {
"name": "webmozart/assert", "name": "webmozart/assert",
@ -4996,5 +5002,5 @@
"ext-json": "*" "ext-json": "*"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.3.0" "plugin-api-version": "2.6.0"
} }

View file

@ -6,6 +6,7 @@
*/ */
use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule; use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\PayLaterConfiguratorModule;
use WooCommerce\PayPalCommerce\PluginModule; use WooCommerce\PayPalCommerce\PluginModule;
return function ( string $root_dir ): iterable { return function ( string $root_dir ): iterable {
@ -67,10 +68,14 @@ return function ( string $root_dir ): iterable {
$modules[] = ( require "$modules_dir/ppcp-save-payment-methods/module.php" )(); $modules[] = ( require "$modules_dir/ppcp-save-payment-methods/module.php" )();
} }
if ( PayLaterBlockModule::is_enabled() ) { if ( PayLaterBlockModule::is_module_loading_required() ) {
$modules[] = ( require "$modules_dir/ppcp-paylater-block/module.php" )(); $modules[] = ( require "$modules_dir/ppcp-paylater-block/module.php" )();
} }
if ( PayLaterConfiguratorModule::is_enabled() ) {
$modules[] = ( require "$modules_dir/ppcp-paylater-configurator/module.php" )();
}
if ( apply_filters( if ( apply_filters(
'woocommerce.feature-flags.woocommerce_paypal_payments.axo_enabled', 'woocommerce.feature-flags.woocommerce_paypal_payments.axo_enabled',
getenv( 'PCP_AXO_ENABLED' ) === '1' getenv( 'PCP_AXO_ENABLED' ) === '1'

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\AdminNotices; namespace WooCommerce\PayPalCommerce\AdminNotices;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
@ -40,6 +42,34 @@ class AdminNotices implements ModuleInterface {
$renderer->render(); $renderer->render();
} }
); );
add_action(
Repository::NOTICES_FILTER,
/**
* Adds persisted notices to the notices array.
*
* @param array $notices The notices.
* @return array
*
* @psalm-suppress MissingClosureParamType
*/
function ( $notices ) use ( $c ) {
if ( ! is_array( $notices ) ) {
return $notices;
}
$admin_notices = $c->get( 'admin-notices.repository' );
assert( $admin_notices instanceof Repository );
$persisted_notices = $admin_notices->get_persisted_and_clear();
if ( $persisted_notices ) {
$notices = array_merge( $notices, $persisted_notices );
}
return $notices;
}
);
} }
/** /**

View file

@ -92,4 +92,18 @@ class Message {
public function wrapper(): string { public function wrapper(): string {
return $this->wrapper; return $this->wrapper;
} }
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
return array(
'type' => $this->type,
'message' => $this->message,
'dismissable' => $this->dismissable,
'wrapper' => $this->wrapper,
);
}
} }

View file

@ -16,7 +16,8 @@ use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
*/ */
class Repository implements RepositoryInterface { class Repository implements RepositoryInterface {
const NOTICES_FILTER = 'ppcp.admin-notices.current-notices'; const NOTICES_FILTER = 'ppcp.admin-notices.current-notices';
const PERSISTED_NOTICES_OPTION = 'woocommerce_ppcp-admin-notices';
/** /**
* Returns the current messages. * Returns the current messages.
@ -37,4 +38,40 @@ class Repository implements RepositoryInterface {
} }
); );
} }
/**
* Adds a message to persist between page reloads.
*
* @param Message $message The message.
* @return void
*/
public function persist( Message $message ): void {
$persisted_notices = get_option( self::PERSISTED_NOTICES_OPTION ) ?: array();
$persisted_notices[] = $message->to_array();
update_option( self::PERSISTED_NOTICES_OPTION, $persisted_notices );
}
/**
* Adds a message to persist between page reloads.
*
* @return array|Message[]
*/
public function get_persisted_and_clear(): array {
$notices = array();
$persisted_data = get_option( self::PERSISTED_NOTICES_OPTION ) ?: array();
foreach ( $persisted_data as $notice_data ) {
$notices[] = new Message(
(string) ( $notice_data['message'] ?? '' ),
(string) ( $notice_data['type'] ?? '' ),
(bool) ( $notice_data['dismissable'] ?? true ),
(string) ( $notice_data['wrapper'] ?? '' )
);
}
update_option( self::PERSISTED_NOTICES_OPTION, array(), true );
return $notices;
}
} }

View file

@ -417,7 +417,7 @@ return array(
return new PaymentsFactory( $authorizations_factory, $capture_factory, $refund_factory ); return new PaymentsFactory( $authorizations_factory, $capture_factory, $refund_factory );
}, },
'api.factory.authorization' => static function ( ContainerInterface $container ): AuthorizationFactory { 'api.factory.authorization' => static function ( ContainerInterface $container ): AuthorizationFactory {
return new AuthorizationFactory(); return new AuthorizationFactory( $container->get( 'api.factory.fraud-processor-response' ) );
}, },
'api.factory.exchange-rate' => static function ( ContainerInterface $container ): ExchangeRateFactory { 'api.factory.exchange-rate' => static function ( ContainerInterface $container ): ExchangeRateFactory {
return new ExchangeRateFactory(); return new ExchangeRateFactory();
@ -626,6 +626,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'AT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'BE' => array( 'BE' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -914,6 +938,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'IE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'IT' => array( 'IT' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -1310,11 +1358,28 @@ return array(
'USD', 'USD',
), ),
'NO' => array( 'NO' => array(
'EUR', 'AUD',
'USD', 'BRL',
'CAD', 'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP', 'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK', 'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
), ),
) )
); );
@ -1335,35 +1400,40 @@ return array(
'visa' => array(), 'visa' => array(),
'amex' => array( 'AUD' ), 'amex' => array( 'AUD' ),
), ),
'AT' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array(),
),
'BE' => array( 'BE' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR', 'USD', 'CAD' ), 'amex' => array(),
), ),
'BG' => array( 'BG' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR' ), 'amex' => array(),
), ),
'CY' => array( 'CY' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR' ), 'amex' => array(),
), ),
'CZ' => array( 'CZ' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'CZK' ), 'amex' => array(),
), ),
'DE' => array( 'DE' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR' ), 'amex' => array(),
), ),
'DK' => array( 'DK' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'DKK' ), 'amex' => array(),
), ),
'EE' => array( 'EE' => array(
'mastercard' => array(), 'mastercard' => array(),
@ -1373,37 +1443,42 @@ return array(
'ES' => array( 'ES' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR' ), 'amex' => array(),
), ),
'FI' => array( 'FI' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR' ), 'amex' => array(),
), ),
'FR' => array( 'FR' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR' ), 'amex' => array(),
), ),
'GB' => array( 'GB' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'GBP', 'USD' ), 'amex' => array(),
), ),
'GR' => array( 'GR' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR' ), 'amex' => array(),
), ),
'HU' => array( 'HU' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'HUF' ), 'amex' => array(),
),
'IE' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array(),
), ),
'IT' => array( 'IT' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR' ), 'amex' => array(),
), ),
'US' => array( 'US' => array(
'mastercard' => array(), 'mastercard' => array(),
@ -1414,28 +1489,33 @@ return array(
'CA' => array( 'CA' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'CAD' ), 'amex' => array( 'CAD', 'USD' ),
'jcb' => array( 'CAD' ), 'jcb' => array( 'CAD' ),
), ),
'LI' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array(),
),
'LT' => array( 'LT' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR' ), 'amex' => array(),
), ),
'LU' => array( 'LU' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR' ), 'amex' => array(),
), ),
'LV' => array( 'LV' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR', 'USD' ), 'amex' => array(),
), ),
'MT' => array( 'MT' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR' ), 'amex' => array(),
), ),
'MX' => array( 'MX' => array(
'mastercard' => array(), 'mastercard' => array(),
@ -1445,42 +1525,42 @@ return array(
'NL' => array( 'NL' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR', 'USD' ), 'amex' => array(),
), ),
'NO' => array( 'NO' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'NOK' ), 'amex' => array(),
), ),
'PL' => array( 'PL' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR', 'USD', 'GBP', 'PLN' ), 'amex' => array(),
), ),
'PT' => array( 'PT' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR', 'USD', 'CAD', 'GBP' ), 'amex' => array(),
), ),
'RO' => array( 'RO' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR', 'USD' ), 'amex' => array(),
), ),
'SE' => array( 'SE' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR', 'SEK' ), 'amex' => array(),
), ),
'SI' => array( 'SI' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR' ), 'amex' => array(),
), ),
'SK' => array( 'SK' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),
'amex' => array( 'EUR', 'GBP' ), 'amex' => array(),
), ),
'JP' => array( 'JP' => array(
'mastercard' => array(), 'mastercard' => array(),

View file

@ -115,7 +115,7 @@ class PaymentMethodTokensEndpoint {
* @throws RuntimeException When something when wrong with the request. * @throws RuntimeException When something when wrong with the request.
* @throws PayPalApiException When something when wrong setting up the token. * @throws PayPalApiException When something when wrong setting up the token.
*/ */
public function payment_tokens( PaymentSource $payment_source ): stdClass { public function create_payment_token( PaymentSource $payment_source ): stdClass {
$data = array( $data = array(
'payment_source' => array( 'payment_source' => array(
$payment_source->name() => $payment_source->properties(), $payment_source->name() => $payment_source->properties(),

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WP_Error; use WP_Error;
@ -92,4 +93,53 @@ class PaymentTokensEndpoint {
throw new PayPalApiException( $json, $status_code ); throw new PayPalApiException( $json, $status_code );
} }
} }
/**
* Returns all payment tokens for the given customer.
*
* @param string $customer_id PayPal customer id.
* @return array
*
* @throws RuntimeException When something went wrong with the request.
* @throws PayPalApiException When something went wrong getting the payment tokens.
*/
public function payment_tokens_for_customer( string $customer_id ): array {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v3/vault/payment-tokens?customer_id=' . $customer_id;
$args = array(
'method' => 'GET',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
throw new PayPalApiException( $json, $status_code );
}
$tokens = array();
$payment_tokens = $json->payment_tokens ?? array();
foreach ( $payment_tokens as $payment_token ) {
$name = array_key_first( (array) $payment_token->payment_source ) ?? '';
if ( $name ) {
$tokens[] = array(
'id' => $payment_token->id,
'payment_source' => new PaymentSource(
$name,
$payment_token->payment_source->$name
),
);
}
}
return $tokens;
}
} }

View file

@ -193,6 +193,53 @@ class PaymentsEndpoint {
return $this->capture_factory->from_paypal_response( $json ); return $this->capture_factory->from_paypal_response( $json );
} }
/**
* Reauthorizes an order.
*
* @param string $authorization_id The id.
* @param Money|null $amount The amount to capture. If not specified, the whole authorized amount is captured.
*
* @return string
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function reauthorize( string $authorization_id, ?Money $amount = null ) : string {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/payments/authorizations/' . $authorization_id . '/reauthorize';
$data = array();
if ( $amount ) {
$data['amount'] = $amount->to_array();
}
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
'body' => wp_json_encode( $data, JSON_FORCE_OBJECT ),
);
$response = $this->request( $url, $args );
$json = json_decode( $response['body'] );
if ( is_wp_error( $response ) ) {
throw new RuntimeException( 'Could not reauthorize authorized payment.' );
}
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code || ! is_object( $json ) ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $json->id;
}
/** /**
* Refunds a payment. * Refunds a payment.
* *

View file

@ -28,19 +28,29 @@ class Authorization {
*/ */
private $authorization_status; private $authorization_status;
/**
* The fraud processor response (AVS, CVV ...).
*
* @var FraudProcessorResponse|null
*/
protected $fraud_processor_response;
/** /**
* Authorization constructor. * Authorization constructor.
* *
* @param string $id The id. * @param string $id The id.
* @param AuthorizationStatus $authorization_status The status. * @param AuthorizationStatus $authorization_status The status.
* @param FraudProcessorResponse|null $fraud_processor_response The fraud processor response (AVS, CVV ...).
*/ */
public function __construct( public function __construct(
string $id, string $id,
AuthorizationStatus $authorization_status AuthorizationStatus $authorization_status,
?FraudProcessorResponse $fraud_processor_response
) { ) {
$this->id = $id; $this->id = $id;
$this->authorization_status = $authorization_status; $this->authorization_status = $authorization_status;
$this->fraud_processor_response = $fraud_processor_response;
} }
/** /**
@ -71,15 +81,30 @@ class Authorization {
$this->authorization_status->is( AuthorizationStatus::PENDING ); $this->authorization_status->is( AuthorizationStatus::PENDING );
} }
/**
* Returns the fraud processor response (AVS, CVV ...).
*
* @return FraudProcessorResponse|null
*/
public function fraud_processor_response() : ?FraudProcessorResponse {
return $this->fraud_processor_response;
}
/** /**
* Returns the object as array. * Returns the object as array.
* *
* @return array * @return array
*/ */
public function to_array(): array { public function to_array(): array {
return array( $data = array(
'id' => $this->id, 'id' => $this->id,
'status' => $this->authorization_status->name(), 'status' => $this->authorization_status->name(),
); );
if ( $this->fraud_processor_response ) {
$data['fraud_processor_response'] = $this->fraud_processor_response->to_array();
}
return $data;
} }
} }

View file

@ -21,19 +21,37 @@ class SellerStatus {
*/ */
private $products; private $products;
/**
* The capabilities.
*
* @var SellerStatusCapability[]
*/
private $capabilities;
/** /**
* SellerStatus constructor. * SellerStatus constructor.
* *
* @param SellerStatusProduct[] $products The products. * @param SellerStatusProduct[] $products The products.
* @param SellerStatusCapability[] $capabilities The capabilities.
*
* @psalm-suppress RedundantConditionGivenDocblockType
*/ */
public function __construct( array $products ) { public function __construct( array $products, array $capabilities ) {
foreach ( $products as $key => $product ) { foreach ( $products as $key => $product ) {
if ( is_a( $product, SellerStatusProduct::class ) ) { if ( is_a( $product, SellerStatusProduct::class ) ) {
continue; continue;
} }
unset( $products[ $key ] ); unset( $products[ $key ] );
} }
$this->products = $products; foreach ( $capabilities as $key => $capability ) {
if ( is_a( $capability, SellerStatusCapability::class ) ) {
continue;
}
unset( $capabilities[ $key ] );
}
$this->products = $products;
$this->capabilities = $capabilities;
} }
/** /**
@ -45,6 +63,15 @@ class SellerStatus {
return $this->products; return $this->products;
} }
/**
* Returns the capabilities.
*
* @return SellerStatusCapability[]
*/
public function capabilities() : array {
return $this->capabilities;
}
/** /**
* Returns the enitity as array. * Returns the enitity as array.
* *
@ -58,8 +85,16 @@ class SellerStatus {
$this->products() $this->products()
); );
$capabilities = array_map(
function( SellerStatusCapability $capability ) : array {
return $capability->to_array();
},
$this->capabilities()
);
return array( return array(
'products' => $products, 'products' => $products,
'capabilities' => $capabilities,
); );
} }
} }

View file

@ -0,0 +1,77 @@
<?php
/**
* The capabilities of a seller status.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class SellerStatusCapability
*/
class SellerStatusCapability {
const STATUS_ACTIVE = 'ACTIVE';
/**
* The name of the product.
*
* @var string
*/
private $name;
/**
* The status of the capability.
*
* @var string
*/
private $status;
/**
* SellerStatusCapability constructor.
*
* @param string $name The name of the product.
* @param string $status The status of the capability.
*/
public function __construct(
string $name,
string $status
) {
$this->name = $name;
$this->status = $status;
}
/**
* Returns the name of the product.
*
* @return string
*/
public function name() : string {
return $this->name;
}
/**
* Returns the status for this capability.
*
* @return string
*/
public function status() : string {
return $this->status;
}
/**
* Returns the entity as array.
*
* @return array
*/
public function to_array() : array {
return array(
'name' => $this->name(),
'status' => $this->status(),
);
}
}

View file

@ -19,6 +19,22 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
*/ */
class AuthorizationFactory { class AuthorizationFactory {
/**
* The FraudProcessorResponseFactory factory.
*
* @var FraudProcessorResponseFactory
*/
protected $fraud_processor_response_factory;
/**
* AuthorizationFactory constructor.
*
* @param FraudProcessorResponseFactory $fraud_processor_response_factory The FraudProcessorResponseFactory factory.
*/
public function __construct( FraudProcessorResponseFactory $fraud_processor_response_factory ) {
$this->fraud_processor_response_factory = $fraud_processor_response_factory;
}
/** /**
* Returns an Authorization based off a PayPal response. * Returns an Authorization based off a PayPal response.
* *
@ -42,12 +58,17 @@ class AuthorizationFactory {
$reason = $data->status_details->reason ?? null; $reason = $data->status_details->reason ?? null;
$fraud_processor_response = isset( $data->processor_response ) ?
$this->fraud_processor_response_factory->from_paypal_response( $data->processor_response )
: null;
return new Authorization( return new Authorization(
$data->id, $data->id,
new AuthorizationStatus( new AuthorizationStatus(
$data->status, $data->status,
$reason ? new AuthorizationStatusDetails( $reason ) : null $reason ? new AuthorizationStatusDetails( $reason ) : null
) ),
$fraud_processor_response
); );
} }
} }

View file

@ -25,8 +25,8 @@ class FraudProcessorResponseFactory {
* @return FraudProcessorResponse * @return FraudProcessorResponse
*/ */
public function from_paypal_response( stdClass $data ): FraudProcessorResponse { public function from_paypal_response( stdClass $data ): FraudProcessorResponse {
$avs_code = $data->avs_code ?: null; $avs_code = ( $data->avs_code ?? null ) ?: null;
$cvv_code = $data->cvv_code ?: null; $cvv_code = ( $data->cvv_code ?? null ) ?: null;
return new FraudProcessorResponse( $avs_code, $cvv_code ); return new FraudProcessorResponse( $avs_code, $cvv_code );
} }

View file

@ -61,12 +61,12 @@ class ItemFactory {
$price = (float) $item['line_subtotal'] / (float) $item['quantity']; $price = (float) $item['line_subtotal'] / (float) $item['quantity'];
return new Item( return new Item(
mb_substr( $product->get_name(), 0, 127 ), $this->prepare_item_string( $product->get_name() ),
new Money( $price, $this->currency ), new Money( $price, $this->currency ),
$quantity, $quantity,
$this->prepare_description( $product->get_description() ), $this->prepare_item_string( $product->get_description() ),
null, null,
$product->get_sku(), $this->prepare_sku( $product->get_sku() ),
( $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS, ( $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS,
$product->get_permalink(), $product->get_permalink(),
$image[0] ?? '', $image[0] ?? '',
@ -138,12 +138,12 @@ class ItemFactory {
$image = $product instanceof WC_Product ? wp_get_attachment_image_src( (int) $product->get_image_id(), 'full' ) : ''; $image = $product instanceof WC_Product ? wp_get_attachment_image_src( (int) $product->get_image_id(), 'full' ) : '';
return new Item( return new Item(
mb_substr( $item->get_name(), 0, 127 ), $this->prepare_item_string( $item->get_name() ),
new Money( $price_without_tax_rounded, $currency ), new Money( $price_without_tax_rounded, $currency ),
$quantity, $quantity,
$product instanceof WC_Product ? $this->prepare_description( $product->get_description() ) : '', $product instanceof WC_Product ? $this->prepare_item_string( $product->get_description() ) : '',
null, null,
$product instanceof WC_Product ? $product->get_sku() : '', $product instanceof WC_Product ? $this->prepare_sku( $product->get_sku() ) : '',
( $product instanceof WC_Product && $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS, ( $product instanceof WC_Product && $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS,
$product instanceof WC_Product ? $product->get_permalink() : '', $product instanceof WC_Product ? $product->get_permalink() : '',
$image[0] ?? '' $image[0] ?? ''
@ -160,7 +160,7 @@ class ItemFactory {
*/ */
private function from_wc_order_fee( \WC_Order_Item_Fee $item, \WC_Order $order ): Item { private function from_wc_order_fee( \WC_Order_Item_Fee $item, \WC_Order $order ): Item {
return new Item( return new Item(
$item->get_name(), $this->prepare_item_string( $item->get_name() ),
new Money( (float) $item->get_amount(), $order->get_currency() ), new Money( (float) $item->get_amount(), $order->get_currency() ),
$item->get_quantity(), $item->get_quantity(),
'', '',

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusProduct; use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusProduct;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusCapability;
/** /**
* Class SellerStatusFactory * Class SellerStatusFactory
@ -37,6 +38,17 @@ class SellerStatusFactory {
isset( $json->products ) ? (array) $json->products : array() isset( $json->products ) ? (array) $json->products : array()
); );
return new SellerStatus( $products ); $capabilities = array_map(
function( $json ) : SellerStatusCapability {
$capability = new SellerStatusCapability(
isset( $json->name ) ? (string) $json->name : '',
isset( $json->status ) ? (string) $json->status : ''
);
return $capability;
},
isset( $json->capabilities ) ? (array) $json->capabilities : array()
);
return new SellerStatus( $products, $capabilities );
} }
} }

View file

@ -12,13 +12,23 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
trait ItemTrait { trait ItemTrait {
/** /**
* Cleanups the description and prepares it for sending to PayPal. * Cleans up item strings (title and description for example) and prepares them for sending to PayPal.
* *
* @param string $description Item description. * @param string $string Item string.
* @return string * @return string
*/ */
protected function prepare_description( string $description ): string { protected function prepare_item_string( string $string ): string {
$description = strip_shortcodes( wp_strip_all_tags( $description ) ); $string = strip_shortcodes( wp_strip_all_tags( $string ) );
return substr( $description, 0, 127 ) ?: ''; return substr( $string, 0, 127 ) ?: '';
}
/**
* Prepares the sku for sending to PayPal.
*
* @param string $sku Item sku.
* @return string
*/
protected function prepare_sku( string $sku ): string {
return substr( wp_strip_all_tags( $sku ), 0, 127 ) ?: '';
} }
} }

View file

@ -70,10 +70,6 @@ class ApplepayButton {
if (this.isEligible) { if (this.isEligible) {
this.fetchTransactionInfo().then(() => { this.fetchTransactionInfo().then(() => {
const isSubscriptionProduct = this.ppcpConfig?.data_client_id?.has_subscriptions === true;
if (isSubscriptionProduct) {
return;
}
this.addButton(); this.addButton();
const id_minicart = "#apple-" + this.buttonConfig.button.mini_cart_wrapper; const id_minicart = "#apple-" + this.buttonConfig.button.mini_cart_wrapper;
const id = "#apple-" + this.buttonConfig.button.wrapper; const id = "#apple-" + this.buttonConfig.button.wrapper;
@ -214,6 +210,8 @@ class ApplepayButton {
const paymentRequest = this.paymentRequest(); const paymentRequest = this.paymentRequest();
window.ppcpFundingSource = 'apple_pay'; // Do this on another place like on create order endpoint handler.
// Trigger woocommerce validation if we are in the checkout page. // Trigger woocommerce validation if we are in the checkout page.
if (this.context === 'checkout') { if (this.context === 'checkout') {
const checkoutFormSelector = 'form.woocommerce-checkout'; const checkoutFormSelector = 'form.woocommerce-checkout';

View file

@ -1,6 +1,7 @@
import ErrorHandler from "../../../../ppcp-button/resources/js/modules/ErrorHandler"; import ErrorHandler from "../../../../ppcp-button/resources/js/modules/ErrorHandler";
import CartActionHandler import CartActionHandler
from "../../../../ppcp-button/resources/js/modules/ActionHandler/CartActionHandler"; from "../../../../ppcp-button/resources/js/modules/ActionHandler/CartActionHandler";
import {isPayPalSubscription} from "../../../../ppcp-blocks/resources/js/Helper/Subscription";
class BaseHandler { class BaseHandler {
@ -9,9 +10,15 @@ class BaseHandler {
this.ppcpConfig = ppcpConfig; this.ppcpConfig = ppcpConfig;
} }
isVaultV3Mode() {
return this.ppcpConfig?.save_payment_methods?.id_token // vault v3
&& ! this.ppcpConfig?.data_client_id?.paypal_subscriptions_enabled // not PayPal Subscriptions mode
&& this.ppcpConfig?.can_save_vault_token; // vault is enabled
}
validateContext() { validateContext() {
if ( this.ppcpConfig?.locations_with_subscription_product?.cart ) { if ( this.ppcpConfig?.locations_with_subscription_product?.cart ) {
return false; return this.isVaultV3Mode();
} }
return true; return true;
} }

View file

@ -7,7 +7,7 @@ class PayNowHandler extends BaseHandler {
validateContext() { validateContext() {
if ( this.ppcpConfig?.locations_with_subscription_product?.payorder ) { if ( this.ppcpConfig?.locations_with_subscription_product?.payorder ) {
return false; return this.isVaultV3Mode();
} }
return true; return true;
} }

View file

@ -9,7 +9,7 @@ class SingleProductHandler extends BaseHandler {
validateContext() { validateContext() {
if ( this.ppcpConfig?.locations_with_subscription_product?.product ) { if ( this.ppcpConfig?.locations_with_subscription_product?.product ) {
return false; return this.isVaultV3Mode();
} }
return true; return true;
} }

View file

@ -1,8 +1,10 @@
import {useEffect, useState} from '@wordpress/element'; import {useEffect, useState} from '@wordpress/element';
import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry'; import {registerExpressPaymentMethod} from '@woocommerce/blocks-registry';
import {loadPaypalScript} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading' import {loadPaypalScript} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'
import {cartHasSubscriptionProducts} from '../../../ppcp-blocks/resources/js/Helper/Subscription'
import ApplepayManager from "./ApplepayManager"; import ApplepayManager from "./ApplepayManager";
import {loadCustomScript} from "@paypal/paypal-js"; import {loadCustomScript} from "@paypal/paypal-js";
import CheckoutHandler from "./Context/CheckoutHandler";
const ppcpData = wc.wcSettings.getSetting('ppcp-gateway_data'); const ppcpData = wc.wcSettings.getSetting('ppcp-gateway_data');
const ppcpConfig = ppcpData.scriptData; const ppcpConfig = ppcpData.scriptData;
@ -50,6 +52,13 @@ const ApplePayComponent = () => {
const features = ['products']; const features = ['products'];
if (
cartHasSubscriptionProducts(ppcpConfig)
&& (new CheckoutHandler(buttonConfig, ppcpConfig)).isVaultV3Mode()
) {
features.push('subscriptions');
}
registerExpressPaymentMethod({ registerExpressPaymentMethod({
name: buttonData.id, name: buttonData.id,
label: <div dangerouslySetInnerHTML={{__html: buttonData.title}}/>, label: <div dangerouslySetInnerHTML={{__html: buttonData.title}}/>,

View file

@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\Applepay\Assets\BlocksPaymentMethod;
use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary; use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary;
use WooCommerce\PayPalCommerce\Applepay\Helper\ApmApplies; use WooCommerce\PayPalCommerce\Applepay\Helper\ApmApplies;
use WooCommerce\PayPalCommerce\Applepay\Helper\AvailabilityNotice; use WooCommerce\PayPalCommerce\Applepay\Helper\AvailabilityNotice;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
@ -72,14 +73,16 @@ return array(
return $settings->has( 'applepay_validated' ) ? $settings->get( 'applepay_validated' ) === true : false; return $settings->has( 'applepay_validated' ) ? $settings->get( 'applepay_validated' ) === true : false;
}, },
'applepay.apple-product-status' => static function( ContainerInterface $container ): AppleProductStatus { 'applepay.apple-product-status' => SingletonDecorator::make(
return new AppleProductStatus( static function( ContainerInterface $container ): AppleProductStatus {
$container->get( 'wcgateway.settings' ), return new AppleProductStatus(
$container->get( 'api.endpoint.partners' ), $container->get( 'wcgateway.settings' ),
$container->get( 'onboarding.state' ), $container->get( 'api.endpoint.partners' ),
$container->get( 'api.helper.failure-registry' ) $container->get( 'onboarding.state' ),
); $container->get( 'api.helper.failure-registry' )
}, );
}
),
'applepay.available' => static function ( ContainerInterface $container ): bool { 'applepay.available' => static function ( ContainerInterface $container ): bool {
if ( apply_filters( 'woocommerce_paypal_payments_applepay_validate_product_status', true ) ) { if ( apply_filters( 'woocommerce_paypal_payments_applepay_validate_product_status', true ) ) {
$status = $container->get( 'applepay.apple-product-status' ); $status = $container->get( 'applepay.apple-product-status' );
@ -195,6 +198,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'AT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'BE' => array( 'BE' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -483,6 +510,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'IE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'IT' => array( 'IT' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -531,6 +582,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'LI' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'LT' => array( 'LT' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -603,6 +678,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'NO' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'NL' => array( 'NL' => array(
'AUD', 'AUD',
'BRL', 'BRL',

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Applepay\Assets;
use Throwable; use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusCapability;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -100,6 +101,11 @@ class AppleProductStatus {
return false; return false;
} }
$status_override = apply_filters( 'woocommerce_paypal_payments_apple_pay_product_status', null );
if ( null !== $status_override ) {
return $status_override;
}
// If status was already checked on this request return the same result. // If status was already checked on this request return the same result.
if ( null !== $this->current_status ) { if ( null !== $this->current_status ) {
return $this->current_status; return $this->current_status;
@ -128,21 +134,32 @@ class AppleProductStatus {
} }
// Check the seller status for the intended capability. // Check the seller status for the intended capability.
$has_capability = false;
foreach ( $seller_status->products() as $product ) { foreach ( $seller_status->products() as $product ) {
if ( $product->name() !== 'PAYMENT_METHODS' ) { if ( $product->name() !== 'PAYMENT_METHODS' ) {
continue; continue;
} }
if ( in_array( self::CAPABILITY_NAME, $product->capabilities(), true ) ) { if ( in_array( self::CAPABILITY_NAME, $product->capabilities(), true ) ) {
// Capability found, persist status and return true. $has_capability = true;
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
$this->settings->persist();
$this->current_status = true;
return $this->current_status;
} }
} }
foreach ( $seller_status->capabilities() as $capability ) {
if ( $capability->name() === self::CAPABILITY_NAME && $capability->status() === SellerStatusCapability::STATUS_ACTIVE ) {
$has_capability = true;
}
}
if ( $has_capability ) {
// Capability found, persist status and return true.
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
$this->settings->persist();
$this->current_status = true;
return $this->current_status;
}
// Capability not found, persist status and return false. // Capability not found, persist status and return false.
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_DISABLED ); $this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_DISABLED );
$this->settings->persist(); $this->settings->persist();

View file

@ -0,0 +1,22 @@
/**
* @param str
* @returns {string}
*/
export const toSnakeCase = (str) => {
return str.replace(/[\w]([A-Z])/g, function(m) {
return m[0] + "_" + m[1];
}).toLowerCase();
}
/**
* @param obj
* @returns {{}}
*/
export const convertKeysToSnakeCase = (obj) => {
const newObj = {};
Object.keys(obj).forEach((key) => {
const newKey = toSnakeCase(key);
newObj[newKey] = obj[key];
});
return newObj;
}

View file

@ -6,6 +6,9 @@ import {
paypalOrderToWcAddresses, paypalOrderToWcAddresses,
paypalSubscriptionToWcAddresses paypalSubscriptionToWcAddresses
} from "./Helper/Address"; } from "./Helper/Address";
import {
convertKeysToSnakeCase
} from "./Helper/Helper";
import { import {
cartHasSubscriptionProducts, cartHasSubscriptionProducts,
isPayPalSubscription isPayPalSubscription
@ -18,6 +21,7 @@ import {
} from '../../../ppcp-button/resources/js/modules/Helper/Style' } from '../../../ppcp-button/resources/js/modules/Helper/Style'
import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher"; import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher";
import BlockCheckoutMessagesBootstrap from "./Bootstrap/BlockCheckoutMessagesBootstrap"; import BlockCheckoutMessagesBootstrap from "./Bootstrap/BlockCheckoutMessagesBootstrap";
import {keysToCamelCase} from "../../../ppcp-button/resources/js/modules/Helper/Utils";
const config = wc.wcSettings.getSetting('ppcp-gateway_data'); const config = wc.wcSettings.getSetting('ppcp-gateway_data');
window.ppcpFundingSource = config.fundingSource; window.ppcpFundingSource = config.fundingSource;
@ -87,6 +91,7 @@ const PayPalComponent = ({
bn_code: '', bn_code: '',
context: config.scriptData.context, context: config.scriptData.context,
payment_method: 'ppcp-gateway', payment_method: 'ppcp-gateway',
funding_source: window.ppcpFundingSource ?? 'paypal',
createaccount: false createaccount: false
}), }),
}); });
@ -285,17 +290,43 @@ const PayPalComponent = ({
onClick(); onClick();
}; };
let handleShippingChange = null; let handleShippingOptionsChange = null;
let handleSubscriptionShippingChange = null; let handleShippingAddressChange = null;
let handleSubscriptionShippingOptionsChange = null;
let handleSubscriptionShippingAddressChange = null;
if (shippingData.needsShipping && !config.finalReviewEnabled) { if (shippingData.needsShipping && !config.finalReviewEnabled) {
handleShippingChange = async (data, actions) => { handleShippingOptionsChange = async (data, actions) => {
try { try {
const shippingOptionId = data.selected_shipping_option?.id; const shippingOptionId = data.selectedShippingOption?.id;
if (shippingOptionId) { if (shippingOptionId) {
await wp.data.dispatch('wc/store/cart').selectShippingRate(shippingOptionId);
await shippingData.setSelectedRates(shippingOptionId); await shippingData.setSelectedRates(shippingOptionId);
} }
const address = paypalAddressToWc(data.shipping_address); const res = await fetch(config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
})
});
const json = await res.json();
if (!json.success) {
throw new Error(json.data.message);
}
} catch (e) {
console.error(e);
actions.reject();
}
};
handleShippingAddressChange = async (data, actions) => {
try {
const address = paypalAddressToWc(convertKeysToSnakeCase(data.shippingAddress));
await wp.data.dispatch('wc/store/cart').updateCustomerData({ await wp.data.dispatch('wc/store/cart').updateCustomerData({
shipping_address: address, shipping_address: address,
@ -324,16 +355,23 @@ const PayPalComponent = ({
} }
}; };
handleSubscriptionShippingChange = async (data, actions) => { handleSubscriptionShippingOptionsChange = async (data, actions) => {
console.log('--- handleSubscriptionShippingChange', data, actions);
try { try {
const shippingOptionId = data.selected_shipping_option?.id; const shippingOptionId = data.selectedShippingOption?.id;
if (shippingOptionId) { if (shippingOptionId) {
await wp.data.dispatch('wc/store/cart').selectShippingRate(shippingOptionId);
await shippingData.setSelectedRates(shippingOptionId); await shippingData.setSelectedRates(shippingOptionId);
} }
} catch (e) {
console.error(e);
const address = paypalAddressToWc(data.shipping_address); actions.reject();
}
};
handleSubscriptionShippingAddressChange = async (data, actions) => {
try {
const address = paypalAddressToWc(convertKeysToSnakeCase(data.shippingAddress));
await wp.data.dispatch('wc/store/cart').updateCustomerData({ await wp.data.dispatch('wc/store/cart').updateCustomerData({
shipping_address: address, shipping_address: address,
@ -443,7 +481,8 @@ const PayPalComponent = ({
onError={onClose} onError={onClose}
createSubscription={createSubscription} createSubscription={createSubscription}
onApprove={handleApproveSubscription} onApprove={handleApproveSubscription}
onShippingChange={handleSubscriptionShippingChange} onShippingOptionsChange={handleSubscriptionShippingOptionsChange}
onShippingAddressChange={handleSubscriptionShippingAddressChange}
/> />
); );
} }
@ -457,7 +496,8 @@ const PayPalComponent = ({
onError={onClose} onError={onClose}
createOrder={createOrder} createOrder={createOrder}
onApprove={handleApprove} onApprove={handleApprove}
onShippingChange={handleShippingChange} onShippingOptionsChange={handleShippingOptionsChange}
onShippingAddressChange={handleShippingAddressChange}
/> />
); );
} }
@ -476,6 +516,14 @@ if(cartHasSubscriptionProducts(config.scriptData)) {
block_enabled = false; block_enabled = false;
} }
// Don't render if vaulting disabled and is in vault subscription mode
if(
! isPayPalSubscription(config.scriptData)
&& ! config.scriptData.can_save_vault_token
) {
block_enabled = false;
}
// Don't render buttons if in subscription mode and product not associated with a PayPal subscription // Don't render buttons if in subscription mode and product not associated with a PayPal subscription
if( if(
isPayPalSubscription(config.scriptData) isPayPalSubscription(config.scriptData)

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Blocks;
use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint; use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
return array( return array(
'blocks.url' => static function ( ContainerInterface $container ): string { 'blocks.url' => static function ( ContainerInterface $container ): string {
@ -28,7 +29,9 @@ return array(
return new PayPalPaymentMethod( return new PayPalPaymentMethod(
$container->get( 'blocks.url' ), $container->get( 'blocks.url' ),
$container->get( 'ppcp.asset-version' ), $container->get( 'ppcp.asset-version' ),
$container->get( 'button.smart-button' ), function () use ( $container ): SmartButtonInterface {
return $container->get( 'button.smart-button' );
},
$container->get( 'wcgateway.settings' ), $container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.settings.status' ), $container->get( 'wcgateway.settings.status' ),
$container->get( 'wcgateway.paypal-gateway' ), $container->get( 'wcgateway.paypal-gateway' ),

View file

@ -41,7 +41,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
/** /**
* The smart button script loading handler. * The smart button script loading handler.
* *
* @var SmartButtonInterface * @var SmartButtonInterface|callable
*/ */
private $smart_button; private $smart_button;
@ -125,25 +125,25 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
/** /**
* Assets constructor. * Assets constructor.
* *
* @param string $module_url The url of this module. * @param string $module_url The url of this module.
* @param string $version The assets version. * @param string $version The assets version.
* @param SmartButtonInterface $smart_button The smart button script loading handler. * @param SmartButtonInterface|callable $smart_button The smart button script loading handler.
* @param Settings $plugin_settings The settings. * @param Settings $plugin_settings The settings.
* @param SettingsStatus $settings_status The Settings status helper. * @param SettingsStatus $settings_status The Settings status helper.
* @param PayPalGateway $gateway The WC gateway. * @param PayPalGateway $gateway The WC gateway.
* @param bool $final_review_enabled Whether the final review is enabled. * @param bool $final_review_enabled Whether the final review is enabled.
* @param CancelView $cancellation_view The cancellation view. * @param CancelView $cancellation_view The cancellation view.
* @param SessionHandler $session_handler The Session handler. * @param SessionHandler $session_handler The Session handler.
* @param bool $add_place_order_method Whether to create a non-express method with the standard "Place order" button. * @param bool $add_place_order_method Whether to create a non-express method with the standard "Place order" button.
* @param bool $use_place_order Whether to use the standard "Place order" button instead of PayPal buttons. * @param bool $use_place_order Whether to use the standard "Place order" button instead of PayPal buttons.
* @param string $place_order_button_text The text for the standard "Place order" button. * @param string $place_order_button_text The text for the standard "Place order" button.
* @param string $place_order_button_description The text for additional "Place order" description. * @param string $place_order_button_description The text for additional "Place order" description.
* @param array $all_funding_sources All existing funding sources for PayPal buttons. * @param array $all_funding_sources All existing funding sources for PayPal buttons.
*/ */
public function __construct( public function __construct(
string $module_url, string $module_url,
string $version, string $version,
SmartButtonInterface $smart_button, $smart_button,
Settings $plugin_settings, Settings $plugin_settings,
SettingsStatus $settings_status, SettingsStatus $settings_status,
PayPalGateway $gateway, PayPalGateway $gateway,
@ -209,7 +209,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
* {@inheritDoc} * {@inheritDoc}
*/ */
public function get_payment_method_data() { public function get_payment_method_data() {
$script_data = $this->smart_button->script_data(); $script_data = $this->smart_button()->script_data();
if ( isset( $script_data['continuation'] ) ) { if ( isset( $script_data['continuation'] ) ) {
$url = add_query_arg( array( CancelController::NONCE => wp_create_nonce( CancelController::NONCE ) ), wc_get_checkout_url() ); $url = add_query_arg( array( CancelController::NONCE => wp_create_nonce( CancelController::NONCE ) ), wc_get_checkout_url() );
@ -267,4 +267,21 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
$screen = get_current_screen(); $screen = get_current_screen();
return $screen && $screen->is_block_editor(); return $screen && $screen->is_block_editor();
} }
/**
* The smart button.
*
* @return SmartButtonInterface
*/
private function smart_button(): SmartButtonInterface {
if ( $this->smart_button instanceof SmartButtonInterface ) {
return $this->smart_button;
}
if ( is_callable( $this->smart_button ) ) {
$this->smart_button = ( $this->smart_button )();
}
return $this->smart_button;
}
} }

View file

@ -137,7 +137,12 @@ const bootstrap = () => {
} }
const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart; const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart;
if (isFreeTrial && data.fundingSource !== 'card' && ! PayPalCommerceGateway.subscription_plan_id) { if (
isFreeTrial
&& data.fundingSource !== 'card'
&& ! PayPalCommerceGateway.subscription_plan_id
&& ! PayPalCommerceGateway.vault_v3_enabled
) {
freeTrialHandler.handle(); freeTrialHandler.handle();
return actions.reject(); return actions.reject();
} }
@ -241,7 +246,6 @@ document.addEventListener(
if (!typeof (PayPalCommerceGateway)) { if (!typeof (PayPalCommerceGateway)) {
console.error('PayPal button could not be configured.'); console.error('PayPal button could not be configured.');
return; return;
return;
} }
if ( if (

View file

@ -144,6 +144,54 @@ class CheckoutActionHandler {
} }
} }
} }
addPaymentMethodConfiguration() {
return {
createVaultSetupToken: async () => {
const response = await fetch(this.config.ajax.create_setup_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: this.config.ajax.create_setup_token.nonce,
})
});
const result = await response.json()
if (result.data.id) {
return result.data.id
}
console.error(result)
},
onApprove: async ({vaultSetupToken}) => {
const response = await fetch(this.config.ajax.create_payment_token_for_guest.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: this.config.ajax.create_payment_token_for_guest.nonce,
vault_setup_token: vaultSetupToken,
})
})
const result = await response.json();
if (result.success === true) {
document.querySelector('#place_order').click()
return;
}
console.error(result)
},
onError: (error) => {
console.error(error)
}
}
}
} }
export default CheckoutActionHandler; export default CheckoutActionHandler;

View file

@ -7,7 +7,6 @@ class ButtonModuleWatcher {
} }
watchContextBootstrap(callable) { watchContextBootstrap(callable) {
console.log('ButtonModuleWatcher.js: watchContextBootstrap', this.contextBootstrapRegistry)
this.contextBootstrapWatchers.push(callable); this.contextBootstrapWatchers.push(callable);
Object.values(this.contextBootstrapRegistry).forEach(callable); Object.values(this.contextBootstrapRegistry).forEach(callable);
} }

View file

@ -116,6 +116,14 @@ class CheckoutBootstap {
return; return;
} }
if(
PayPalCommerceGateway.is_free_trial_cart
&& PayPalCommerceGateway.vault_v3_enabled
) {
this.renderer.render(actionHandler.addPaymentMethodConfiguration(), {}, actionHandler.configuration());
return;
}
this.renderer.render(actionHandler.configuration(), {}, actionHandler.configuration()); this.renderer.render(actionHandler.configuration(), {}, actionHandler.configuration());
} }

View file

@ -127,19 +127,7 @@ class CardFieldsRenderer {
const paymentToken = document.querySelector('input[name="wc-ppcp-credit-card-gateway-payment-token"]:checked')?.value const paymentToken = document.querySelector('input[name="wc-ppcp-credit-card-gateway-payment-token"]:checked')?.value
if(paymentToken && paymentToken !== 'new') { if(paymentToken && paymentToken !== 'new') {
fetch(this.defaultConfig.ajax.capture_card_payment.endpoint, { document.querySelector('#place_order').click();
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.defaultConfig.ajax.capture_card_payment.nonce,
payment_token: paymentToken
})
}).then((res) => {
return res.json();
}).then((data) => {
document.querySelector('#place_order').click();
});
return; return;
} }

View file

@ -145,6 +145,8 @@ return array(
$container->get( 'button.early-wc-checkout-validation-enabled' ), $container->get( 'button.early-wc-checkout-validation-enabled' ),
$container->get( 'button.pay-now-contexts' ), $container->get( 'button.pay-now-contexts' ),
$container->get( 'wcgateway.funding-sources-without-redirect' ), $container->get( 'wcgateway.funding-sources-without-redirect' ),
$container->get( 'vaulting.vault-v3-enabled' ),
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },

View file

@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface;
use WC_Order; use WC_Order;
use WC_Product; use WC_Product;
use WC_Product_Variation; use WC_Product_Variation;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
@ -33,6 +34,9 @@ use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule; use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
@ -184,13 +188,6 @@ class SmartButton implements SmartButtonInterface {
*/ */
private $funding_sources_without_redirect; private $funding_sources_without_redirect;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/** /**
* Session handler. * Session handler.
* *
@ -198,6 +195,27 @@ class SmartButton implements SmartButtonInterface {
*/ */
private $session_handler; private $session_handler;
/**
* Whether Vault v3 module is enabled.
*
* @var bool
*/
private $vault_v3_enabled;
/**
* Payment tokens endpoint.
*
* @var PaymentTokensEndpoint
*/
private $payment_tokens_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/** /**
* SmartButton constructor. * SmartButton constructor.
* *
@ -220,6 +238,8 @@ class SmartButton implements SmartButtonInterface {
* @param bool $early_validation_enabled Whether to execute WC validation of the checkout form. * @param bool $early_validation_enabled Whether to execute WC validation of the checkout form.
* @param array $pay_now_contexts The contexts that should have the Pay Now button. * @param array $pay_now_contexts The contexts that should have the Pay Now button.
* @param string[] $funding_sources_without_redirect The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back. * @param string[] $funding_sources_without_redirect The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back.
* @param bool $vault_v3_enabled Whether Vault v3 module is enabled.
* @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
*/ */
public function __construct( public function __construct(
@ -242,6 +262,8 @@ class SmartButton implements SmartButtonInterface {
bool $early_validation_enabled, bool $early_validation_enabled,
array $pay_now_contexts, array $pay_now_contexts,
array $funding_sources_without_redirect, array $funding_sources_without_redirect,
bool $vault_v3_enabled,
PaymentTokensEndpoint $payment_tokens_endpoint,
LoggerInterface $logger LoggerInterface $logger
) { ) {
@ -264,7 +286,9 @@ class SmartButton implements SmartButtonInterface {
$this->early_validation_enabled = $early_validation_enabled; $this->early_validation_enabled = $early_validation_enabled;
$this->pay_now_contexts = $pay_now_contexts; $this->pay_now_contexts = $pay_now_contexts;
$this->funding_sources_without_redirect = $funding_sources_without_redirect; $this->funding_sources_without_redirect = $funding_sources_without_redirect;
$this->vault_v3_enabled = $vault_v3_enabled;
$this->logger = $logger; $this->logger = $logger;
$this->payment_tokens_endpoint = $payment_tokens_endpoint;
} }
/** /**
@ -631,7 +655,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
$messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $location ); $messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $location );
$has_paylater_block = has_block( 'woocommerce-paypal-payments/paylater-messages' ) && PayLaterBlockModule::is_enabled(); $has_paylater_block = has_block( 'woocommerce-paypal-payments/paylater-messages' ) && PayLaterBlockModule::is_block_enabled( $this->settings_status );
switch ( $location ) { switch ( $location ) {
case 'checkout': case 'checkout':
@ -872,12 +896,13 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
$text_color = $this->settings->has( "{$setting_name_prefix}_color" ) ? $this->settings->get( "{$setting_name_prefix}_color" ) : 'black'; $text_color = $this->settings->has( "{$setting_name_prefix}_color" ) ? $this->settings->get( "{$setting_name_prefix}_color" ) : 'black';
$style_color = $this->settings->has( "{$setting_name_prefix}_flex_color" ) ? $this->settings->get( "{$setting_name_prefix}_flex_color" ) : 'blue'; $style_color = $this->settings->has( "{$setting_name_prefix}_flex_color" ) ? $this->settings->get( "{$setting_name_prefix}_flex_color" ) : 'blue';
$ratio = $this->settings->has( "{$setting_name_prefix}_flex_ratio" ) ? $this->settings->get( "{$setting_name_prefix}_flex_ratio" ) : '1x1'; $ratio = $this->settings->has( "{$setting_name_prefix}_flex_ratio" ) ? $this->settings->get( "{$setting_name_prefix}_flex_ratio" ) : '1x1';
$text_size = $this->settings->has( "{$setting_name_prefix}_text_size" ) ? $this->settings->get( "{$setting_name_prefix}_text_size" ) : '12';
return array( return array(
'wrapper' => '#ppcp-messages', 'wrapper' => '#ppcp-messages',
'is_hidden' => ! $this->is_pay_later_filter_enabled_for_location( $this->context() ), 'is_hidden' => ! $this->is_pay_later_filter_enabled_for_location( $this->context() ),
'block' => array( 'block' => array(
'enabled' => PayLaterBlockModule::is_enabled(), 'enabled' => PayLaterBlockModule::is_block_enabled( $this->settings_status ),
), ),
'amount' => $amount, 'amount' => $amount,
'placement' => $placement, 'placement' => $placement,
@ -889,6 +914,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
), ),
'text' => array( 'text' => array(
'color' => $text_color, 'color' => $text_color,
'size' => $text_size,
), ),
'color' => $style_color, 'color' => $style_color,
'ratio' => $ratio, 'ratio' => $ratio,
@ -948,7 +974,10 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* @return bool * @return bool
*/ */
private function has_subscriptions(): bool { private function has_subscriptions(): bool {
if ( ! $this->subscription_helper->accept_only_automatic_payment_gateways() ) { if (
! $this->subscription_helper->accept_only_automatic_payment_gateways()
&& $this->paypal_subscriptions_enabled() !== true
) {
return false; return false;
} }
if ( is_product() ) { if ( is_product() ) {
@ -985,11 +1014,21 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
if ( $this->settings->has( '3d_secure_contingency' ) ) { if ( $this->settings->has( '3d_secure_contingency' ) ) {
$value = $this->settings->get( '3d_secure_contingency' ); $value = $this->settings->get( '3d_secure_contingency' );
if ( $value ) { if ( $value ) {
return $value; return $this->return_3ds_contingency( $value );
} }
} }
return 'SCA_WHEN_REQUIRED'; return $this->return_3ds_contingency( 'SCA_WHEN_REQUIRED' );
}
/**
* Processes and returns the 3D Secure contingency.
*
* @param string $contingency The ThreeD secure contingency.
* @return string
*/
private function return_3ds_contingency( string $contingency ): string {
return apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $contingency );
} }
/** /**
@ -1020,44 +1059,57 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
'redirect' => wc_get_checkout_url(), 'redirect' => wc_get_checkout_url(),
'context' => $this->context(), 'context' => $this->context(),
'ajax' => array( 'ajax' => array(
'simulate_cart' => array( 'simulate_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( SimulateCartEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( SimulateCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SimulateCartEndpoint::nonce() ), 'nonce' => wp_create_nonce( SimulateCartEndpoint::nonce() ),
), ),
'change_cart' => array( 'change_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ), 'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ),
), ),
'create_order' => array( 'create_order' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ), 'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ),
), ),
'approve_order' => array( 'approve_order' => array(
'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ), 'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ),
), ),
'approve_subscription' => array( 'approve_subscription' => array(
'endpoint' => \WC_AJAX::get_endpoint( ApproveSubscriptionEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ApproveSubscriptionEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveSubscriptionEndpoint::nonce() ), 'nonce' => wp_create_nonce( ApproveSubscriptionEndpoint::nonce() ),
), ),
'vault_paypal' => array( 'vault_paypal' => array(
'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ), 'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ),
), ),
'save_checkout_form' => array( 'save_checkout_form' => array(
'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ), 'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ),
), ),
'validate_checkout' => array( 'validate_checkout' => array(
'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ), 'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ),
), ),
'cart_script_params' => array( 'cart_script_params' => array(
'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ),
), ),
'create_setup_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreateSetupToken::nonce() ),
),
'create_payment_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreatePaymentToken::nonce() ),
),
'create_payment_token_for_guest' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentTokenForGuest::ENDPOINT ),
'nonce' => wp_create_nonce( CreatePaymentTokenForGuest::nonce() ),
),
), ),
'cart_contains_subscription' => $this->subscription_helper->cart_contains_subscription(), 'cart_contains_subscription' => $this->subscription_helper->cart_contains_subscription(),
'subscription_plan_id' => $this->subscription_helper->paypal_subscription_id(), 'subscription_plan_id' => $this->subscription_helper->paypal_subscription_id(),
'vault_v3_enabled' => $this->vault_v3_enabled,
'variable_paypal_subscription_variations' => $this->subscription_helper->variable_paypal_subscription_variations(), 'variable_paypal_subscription_variations' => $this->subscription_helper->variable_paypal_subscription_variations(),
'subscription_product_allowed' => $this->subscription_helper->checkout_subscription_product_allowed(), 'subscription_product_allowed' => $this->subscription_helper->checkout_subscription_product_allowed(),
'locations_with_subscription_product' => $this->subscription_helper->locations_with_subscription_product(), 'locations_with_subscription_product' => $this->subscription_helper->locations_with_subscription_product(),
@ -1313,7 +1365,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
$disable_funding, $disable_funding,
array_diff( array_diff(
array_keys( $this->all_funding_sources ), array_keys( $this->all_funding_sources ),
array( 'venmo', 'paylater' ) array( 'venmo', 'paylater', 'paypal' )
) )
); );
} }
@ -1334,6 +1386,20 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
$disable_funding[] = 'paylater'; $disable_funding[] = 'paylater';
} }
$disable_funding = array_filter(
$disable_funding,
/**
* Make sure paypal is not sent in disable funding.
*
* @param string $funding_source The funding_source.
*
* @psalm-suppress MissingClosureParamType
*/
function( $funding_source ) {
return $funding_source !== 'paypal';
}
);
if ( count( $disable_funding ) > 0 ) { if ( count( $disable_funding ) > 0 ) {
$params['disable-funding'] = implode( ',', array_unique( $disable_funding ) ); $params['disable-funding'] = implode( ',', array_unique( $disable_funding ) );
} }
@ -1875,8 +1941,18 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
*/ */
private function get_vaulted_paypal_email(): string { private function get_vaulted_paypal_email(): string {
try { try {
$tokens = $this->get_payment_tokens(); $customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
if ( $customer_id ) {
$customer_tokens = $this->payment_tokens_endpoint->payment_tokens_for_customer( $customer_id );
foreach ( $customer_tokens as $token ) {
$email_address = $token['payment_source']->properties()->email_address ?? '';
if ( $email_address ) {
return $email_address;
}
}
}
$tokens = $this->get_payment_tokens();
foreach ( $tokens as $token ) { foreach ( $tokens as $token ) {
if ( isset( $token->source()->paypal ) ) { if ( isset( $token->source()->paypal ) ) {
return $token->source()->paypal->payer->email_address; return $token->source()->paypal->payer->email_address;
@ -1885,6 +1961,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
} catch ( Exception $exception ) { } catch ( Exception $exception ) {
$this->logger->error( 'Failed to get PayPal vaulted email. ' . $exception->getMessage() ); $this->logger->error( 'Failed to get PayPal vaulted email. ' . $exception->getMessage() );
} }
return ''; return '';
} }

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Throwable; use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface; use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
/** /**
@ -66,6 +67,11 @@ class CartScriptParamsEndpoint implements EndpointInterface {
*/ */
public function handle_request(): bool { public function handle_request(): bool {
try { try {
if ( ! $this->smart_button instanceof SmartButton ) {
wp_send_json_error();
return false;
}
if ( is_callable( 'wc_maybe_define_constant' ) ) { if ( is_callable( 'wc_maybe_define_constant' ) ) {
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true ); wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
} }

View file

@ -329,6 +329,21 @@ class CreateOrderEndpoint implements EndpointInterface {
if ( 'pay-now' === $data['context'] && is_a( $wc_order, \WC_Order::class ) ) { if ( 'pay-now' === $data['context'] && is_a( $wc_order, \WC_Order::class ) ) {
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() ); $wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
$wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() ); $wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() );
$payment_source = $order->payment_source();
$payment_source_name = $payment_source ? $payment_source->name() : null;
$payer = $order->payer();
if (
$payer
&& $payment_source_name
&& in_array( $payment_source_name, PayPalGateway::PAYMENT_SOURCES_WITH_PAYER_EMAIL, true )
) {
$payer_email = $payer->email_address();
if ( $payer_email ) {
$wc_order->update_meta_data( PayPalGateway::ORDER_PAYER_EMAIL_META_KEY, $payer_email );
}
}
$wc_order->save_meta_data(); $wc_order->save_meta_data();
do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order ); do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order );
@ -422,6 +437,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* *
* @throws RuntimeException If create order request fails. * @throws RuntimeException If create order request fails.
* @throws PayPalApiException If create order request fails. * @throws PayPalApiException If create order request fails.
*
* phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber * phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
*/ */
private function create_paypal_order( \WC_Order $wc_order = null, string $payment_method = '', array $data = array() ): Order { private function create_paypal_order( \WC_Order $wc_order = null, string $payment_method = '', array $data = array() ): Order {

View file

@ -13,6 +13,7 @@ use Exception;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton; use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper; use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper;
/** /**
@ -25,7 +26,7 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
/** /**
* The SmartButton. * The SmartButton.
* *
* @var SmartButton * @var SmartButtonInterface
*/ */
private $smart_button; private $smart_button;
@ -39,14 +40,14 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
/** /**
* ChangeCartEndpoint constructor. * ChangeCartEndpoint constructor.
* *
* @param SmartButton $smart_button The SmartButton. * @param SmartButtonInterface $smart_button The SmartButton.
* @param \WC_Cart $cart The current WC cart object. * @param \WC_Cart $cart The current WC cart object.
* @param RequestData $request_data The request data helper. * @param RequestData $request_data The request data helper.
* @param CartProductsHelper $cart_products The cart products helper. * @param CartProductsHelper $cart_products The cart products helper.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
*/ */
public function __construct( public function __construct(
SmartButton $smart_button, SmartButtonInterface $smart_button,
\WC_Cart $cart, \WC_Cart $cart,
RequestData $request_data, RequestData $request_data,
CartProductsHelper $cart_products, CartProductsHelper $cart_products,
@ -68,6 +69,11 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
* @throws Exception On error. * @throws Exception On error.
*/ */
protected function handle_data(): bool { protected function handle_data(): bool {
if ( ! $this->smart_button instanceof SmartButton ) {
wp_send_json_error();
return false;
}
$products = $this->products_from_request(); $products = $this->products_from_request();
if ( ! $products ) { if ( ! $products ) {

View file

@ -159,6 +159,22 @@ class EarlyOrderHandler {
$wc_order = wc_get_order( $order_id ); $wc_order = wc_get_order( $order_id );
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() ); $wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
$wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() ); $wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() );
$payment_source = $order->payment_source();
$payment_source_name = $payment_source ? $payment_source->name() : null;
$payer = $order->payer();
if (
$payer
&& $payment_source_name
&& in_array( $payment_source_name, PayPalGateway::PAYMENT_SOURCES_WITH_PAYER_EMAIL, true )
&& $wc_order instanceof \WC_Order
) {
$payer_email = $payer->email_address();
if ( $payer_email ) {
$wc_order->update_meta_data( PayPalGateway::ORDER_PAYER_EMAIL_META_KEY, $payer_email );
}
}
$wc_order->save_meta_data(); $wc_order->save_meta_data();
/** /**

View file

@ -57,42 +57,58 @@ class ThreeDSecure {
* *
* @link https://developer.paypal.com/docs/business/checkout/add-capabilities/3d-secure/#authenticationresult * @link https://developer.paypal.com/docs/business/checkout/add-capabilities/3d-secure/#authenticationresult
* *
* @param Order $order The order for which the decission is needed. * @param Order $order The order for which the decision is needed.
* *
* @return int * @return int
*/ */
public function proceed_with_order( Order $order ): int { public function proceed_with_order( Order $order ): int {
do_action( 'woocommerce_paypal_payments_three_d_secure_before_check', $order );
$payment_source = $order->payment_source(); $payment_source = $order->payment_source();
if ( ! $payment_source ) { if ( ! $payment_source ) {
return self::NO_DECISION; return $this->return_decision( self::NO_DECISION, $order );
} }
if ( ! $payment_source->properties()->brand ?? '' ) { if ( ! ( $payment_source->properties()->brand ?? '' ) ) {
return self::NO_DECISION; return $this->return_decision( self::NO_DECISION, $order );
} }
if ( ! $payment_source->properties()->authentication_result ?? '' ) { if ( ! ( $payment_source->properties()->authentication_result ?? '' ) ) {
return self::NO_DECISION; return $this->return_decision( self::NO_DECISION, $order );
} }
$authentication_result = $payment_source->properties()->authentication_result ?? null; $authentication_result = $payment_source->properties()->authentication_result ?? null;
if ( $authentication_result ) { if ( $authentication_result ) {
$result = $this->card_authentication_result_factory->from_paypal_response( $authentication_result ); $result = $this->card_authentication_result_factory->from_paypal_response( $authentication_result );
$this->logger->info( '3DS authentication result: ' . wc_print_r( $result->to_array(), true ) ); $this->logger->info( '3DS Authentication Result: ' . wc_print_r( $result->to_array(), true ) );
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) { if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) {
return self::PROCCEED; return $this->return_decision( self::PROCCEED, $order );
} }
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_UNKNOWN ) { if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_UNKNOWN ) {
return self::RETRY; return $this->return_decision( self::RETRY, $order );
} }
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) { if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) {
return $this->no_liability_shift( $result ); return $this->return_decision( $this->no_liability_shift( $result ), $order );
} }
} }
return self::NO_DECISION; return $this->return_decision( self::NO_DECISION, $order );
}
/**
* Processes and returns a ThreeD secure decision.
*
* @param int $decision The ThreeD secure decision.
* @param Order $order The PayPal Order object.
* @return int
*/
public function return_decision( int $decision, Order $order ) {
$decision = apply_filters( 'woocommerce_paypal_payments_three_d_secure_decision', $decision, $order );
do_action( 'woocommerce_paypal_payments_three_d_secure_after_check', $order, $decision );
return $decision;
} }
/** /**

View file

@ -54,6 +54,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'AT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'BE' => array( 'BE' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -342,6 +366,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'IE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'IT' => array( 'IT' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -390,6 +438,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'LI' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'LT' => array( 'LT' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -686,6 +758,30 @@ return array(
'JPY', 'JPY',
'USD', 'USD',
), ),
'NO' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
) )
); );
}, },

View file

@ -115,17 +115,19 @@ class CardFieldsModule implements ModuleInterface {
$settings = $c->get( 'wcgateway.settings' ); $settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings ); assert( $settings instanceof Settings );
$three_d_secure_contingency =
$settings->has( '3d_secure_contingency' )
? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) )
: '';
if ( if (
$settings->has( '3d_secure_contingency' ) $three_d_secure_contingency === 'SCA_ALWAYS'
&& ( || $three_d_secure_contingency === 'SCA_WHEN_REQUIRED'
$settings->get( '3d_secure_contingency' ) === 'SCA_ALWAYS'
|| $settings->get( '3d_secure_contingency' ) === 'SCA_WHEN_REQUIRED'
)
) { ) {
$data['payment_source']['card'] = array( $data['payment_source']['card'] = array(
'attributes' => array( 'attributes' => array(
'verification' => array( 'verification' => array(
'method' => $settings->get( '3d_secure_contingency' ), 'method' => $three_d_secure_contingency,
), ),
), ),
); );

View file

@ -114,6 +114,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'AT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'BE' => array( 'BE' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -402,6 +426,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'IE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'IT' => array( 'IT' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -450,6 +498,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'LI' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'LT' => array( 'LT' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -522,6 +594,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'NO' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'NL' => array( 'NL' => array(
'AUD', 'AUD',
'BRL', 'BRL',

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Googlepay\Helper;
use Throwable; use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusCapability;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -100,6 +101,11 @@ class ApmProductStatus {
return false; return false;
} }
$status_override = apply_filters( 'woocommerce_paypal_payments_google_pay_product_status', null );
if ( null !== $status_override ) {
return $status_override;
}
// If status was already checked on this request return the same result. // If status was already checked on this request return the same result.
if ( null !== $this->current_status ) { if ( null !== $this->current_status ) {
return $this->current_status; return $this->current_status;
@ -128,21 +134,32 @@ class ApmProductStatus {
} }
// Check the seller status for the intended capability. // Check the seller status for the intended capability.
$has_capability = false;
foreach ( $seller_status->products() as $product ) { foreach ( $seller_status->products() as $product ) {
if ( $product->name() !== 'PAYMENT_METHODS' ) { if ( $product->name() !== 'PAYMENT_METHODS' ) {
continue; continue;
} }
if ( in_array( self::CAPABILITY_NAME, $product->capabilities(), true ) ) { if ( in_array( self::CAPABILITY_NAME, $product->capabilities(), true ) ) {
// Capability found, persist status and return true. $has_capability = true;
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
$this->settings->persist();
$this->current_status = true;
return $this->current_status;
} }
} }
foreach ( $seller_status->capabilities() as $capability ) {
if ( $capability->name() === self::CAPABILITY_NAME && $capability->status() === SellerStatusCapability::STATUS_ACTIVE ) {
$has_capability = true;
}
}
if ( $has_capability ) {
// Capability found, persist status and return true.
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
$this->settings->persist();
$this->current_status = true;
return $this->current_status;
}
// Capability not found, persist status and return false. // Capability not found, persist status and return false.
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_DISABLED ); $this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_DISABLED );
$this->settings->persist(); $this->settings->persist();

View file

@ -359,6 +359,20 @@ document.addEventListener(
); );
} }
const referenceTransactionsCheck = () => {
if (
typeof PayPalCommerceGatewaySettings !== 'undefined'
&& PayPalCommerceGatewaySettings.reference_transaction_enabled !== '1'
) {
document.getElementById('ppcp-vault_enabled')?.setAttribute('disabled', 'disabled');
const description = document.getElementById('field-vault_enabled')?.getElementsByClassName('description')[0];
if (description) {
description.innerHTML = PayPalCommerceGatewaySettings.vaulting_must_enable_advanced_wallet_message;
}
}
}
(() => { (() => {
removeDisabledCardIcons('select[name="ppcp[disable_cards][]"]', 'select[name="ppcp[card_icons][]"]'); removeDisabledCardIcons('select[name="ppcp[disable_cards][]"]', 'select[name="ppcp[card_icons][]"]');
@ -408,6 +422,8 @@ document.addEventListener(
); );
togglePayLaterMessageFields(); togglePayLaterMessageFields();
referenceTransactionsCheck()
})(); })();
} }
) )

View file

@ -14,6 +14,7 @@ use WC_Order_Item_Product;
use WC_Product; use WC_Product;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item; use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ItemTrait;
use WooCommerce\PayPalCommerce\OrderTracking\OrderTrackingModule; use WooCommerce\PayPalCommerce\OrderTracking\OrderTrackingModule;
/** /**
@ -21,6 +22,8 @@ use WooCommerce\PayPalCommerce\OrderTracking\OrderTrackingModule;
*/ */
class Shipment implements ShipmentInterface { class Shipment implements ShipmentInterface {
use ItemTrait;
/** /**
* The WC order ID. * The WC order ID.
* *
@ -166,12 +169,12 @@ class Shipment implements ShipmentInterface {
$image = wp_get_attachment_image_src( (int) $product->get_image_id(), 'full' ); $image = wp_get_attachment_image_src( (int) $product->get_image_id(), 'full' );
$ppcp_order_item = new Item( $ppcp_order_item = new Item(
mb_substr( $item->get_name(), 0, 127 ), $this->prepare_item_string( $item->get_name() ),
new Money( $price_without_tax_rounded, $currency ), new Money( $price_without_tax_rounded, $currency ),
$quantity, $quantity,
$this->prepare_description( $product->get_description() ), $this->prepare_item_string( $product->get_description() ),
null, null,
$product->get_sku(), $this->prepare_sku( $product->get_sku() ),
$product->is_virtual() ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS, $product->is_virtual() ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS,
$product->get_permalink(), $product->get_permalink(),
$image[0] ?? '' $image[0] ?? ''
@ -239,17 +242,6 @@ class Shipment implements ShipmentInterface {
return $shipment; return $shipment;
} }
/**
* Cleanups the description and prepares it for sending to PayPal.
*
* @param string $description Item description.
* @return string
*/
protected function prepare_description( string $description ): string {
$description = strip_shortcodes( wp_strip_all_tags( $description ) );
return substr( $description, 0, 127 ) ?: '';
}
/** /**
* Renders the shipment line items info. * Renders the shipment line items info.
* *

View file

@ -7,7 +7,7 @@ import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Help
import PayPalMessages from "./components/PayPalMessages"; import PayPalMessages from "./components/PayPalMessages";
export default function Edit( { attributes, clientId, setAttributes } ) { export default function Edit( { attributes, clientId, setAttributes } ) {
const { layout, logo, position, color, flexColor, flexRatio, placement, id } = attributes; const { layout, logo, position, color, size, flexColor, flexRatio, placement, id } = attributes;
const isFlex = layout === 'flex'; const isFlex = layout === 'flex';
const [paypalScriptState, setPaypalScriptState] = useState(null); const [paypalScriptState, setPaypalScriptState] = useState(null);
@ -30,11 +30,12 @@ export default function Edit( { attributes, clientId, setAttributes } ) {
ratio: flexRatio, ratio: flexRatio,
text: { text: {
color, color,
size
}, },
}; };
let classes = ['ppcp-paylater-block-preview', 'ppcp-overlay-parent']; let classes = ['ppcp-paylater-block-preview', 'ppcp-overlay-parent'];
if (PcpPayLaterBlock.vaultingEnabled) { if (PcpPayLaterBlock.vaultingEnabled || !PcpPayLaterBlock.placementEnabled) {
classes = ['ppcp-paylater-block-preview', 'ppcp-paylater-unavailable', 'block-editor-warning']; classes = ['ppcp-paylater-block-preview', 'ppcp-paylater-unavailable', 'block-editor-warning'];
} }
const props = useBlockProps({className: classes}); const props = useBlockProps({className: classes});
@ -68,6 +69,27 @@ export default function Edit( { attributes, clientId, setAttributes } ) {
</div> </div>
} }
if (!PcpPayLaterBlock.placementEnabled) {
return <div {...props}>
<div className={'block-editor-warning__contents'}>
<h3>{__('PayPal Pay Later Messaging', 'woocommerce-paypal-payments')}</h3>
<p className={'block-editor-warning__message'}>{__('Pay Later Messaging cannot be used while the “WooCommerce Block” messaging placement is disabled. Enable the placement in the PayPal Payments Pay Later settings to reactivate this block.', 'woocommerce-paypal-payments')}</p>
<div className={'class="block-editor-warning__actions"'}>
<span className={'block-editor-warning__action'}>
<a href={PcpPayLaterBlock.payLaterSettingsUrl} className={'components-button is-primary'}>
{__('PayPal Payments Settings', 'woocommerce-paypal-payments')}
</a>
</span>
<span className={'block-editor-warning__action'}>
<button onClick={() => wp.data.dispatch( 'core/block-editor' ).removeBlock(clientId)} type={'button'} className={'components-button is-secondary'}>
{__('Remove Block', 'woocommerce-paypal-payments')}
</button>
</span>
</div>
</div>
</div>
}
let scriptParams = useScriptParams(PcpPayLaterBlock.ajax.cart_script_params); let scriptParams = useScriptParams(PcpPayLaterBlock.ajax.cart_script_params);
if (scriptParams === null) { if (scriptParams === null) {
return loadingElement; return loadingElement;
@ -108,10 +130,10 @@ export default function Edit( { attributes, clientId, setAttributes } ) {
{ !isFlex && (<SelectControl { !isFlex && (<SelectControl
label={__('Logo', 'woocommerce-paypal-payments')} label={__('Logo', 'woocommerce-paypal-payments')}
options={[ options={[
{ label: __('Primary', 'woocommerce-paypal-payments'), value: 'primary' }, { label: __('Full logo', 'woocommerce-paypal-payments'), value: 'primary' },
{ label: __('Alternative', 'woocommerce-paypal-payments'), value: 'alternative' }, { label: __('Monogram', 'woocommerce-paypal-payments'), value: 'alternative' },
{ label: __('Inline', 'woocommerce-paypal-payments'), value: 'inline' }, { label: __('Inline', 'woocommerce-paypal-payments'), value: 'inline' },
{ label: __('None', 'woocommerce-paypal-payments'), value: 'none' }, { label: __('Message only', 'woocommerce-paypal-payments'), value: 'none' },
]} ]}
value={logo} value={logo}
onChange={(value) => setAttributes({logo: value})} onChange={(value) => setAttributes({logo: value})}
@ -129,24 +151,31 @@ export default function Edit( { attributes, clientId, setAttributes } ) {
{ !isFlex && (<SelectControl { !isFlex && (<SelectControl
label={__('Text Color', 'woocommerce-paypal-payments')} label={__('Text Color', 'woocommerce-paypal-payments')}
options={[ options={[
{ label: __( 'Black', 'woocommerce-paypal-payments' ), value: 'black' }, { label: __( 'Black / Blue logo', 'woocommerce-paypal-payments' ), value: 'black' },
{ label: __( 'White', 'woocommerce-paypal-payments' ), value: 'white' }, { label: __( 'White / White logo', 'woocommerce-paypal-payments' ), value: 'white' },
{ label: __( 'Monochrome', 'woocommerce-paypal-payments' ), value: 'monochrome' }, { label: __( 'Monochrome', 'woocommerce-paypal-payments' ), value: 'monochrome' },
{ label: __( 'Grayscale', 'woocommerce-paypal-payments' ), value: 'grayscale' }, { label: __( 'Black / Gray logo', 'woocommerce-paypal-payments' ), value: 'grayscale' },
]} ]}
value={color} value={color}
onChange={(value) => setAttributes({color: value})} onChange={(value) => setAttributes({color: value})}
/>)} />)}
{ !isFlex && (<SelectControl
label={__('Text Size', 'woocommerce-paypal-payments')}
options={[
{ label: __( 'Small', 'woocommerce-paypal-payments' ), value: '12' },
{ label: __( 'Medium', 'woocommerce-paypal-payments' ), value: '14' },
{ label: __( 'Large', 'woocommerce-paypal-payments' ), value: '16' },
]}
value={size}
onChange={(value) => setAttributes({size: value})}
/>)}
{ isFlex && (<SelectControl { isFlex && (<SelectControl
label={__('Color', 'woocommerce-paypal-payments')} label={__('Color', 'woocommerce-paypal-payments')}
options={[ options={[
{ label: __( 'Blue', 'woocommerce-paypal-payments' ), value: 'blue' }, { label: __( 'Blue', 'woocommerce-paypal-payments' ), value: 'blue' },
{ label: __( 'Black', 'woocommerce-paypal-payments' ), value: 'black' }, { label: __( 'Black', 'woocommerce-paypal-payments' ), value: 'black' },
{ label: __( 'White', 'woocommerce-paypal-payments' ), value: 'white' }, { label: __( 'White', 'woocommerce-paypal-payments' ), value: 'white' },
{ label: __( 'White no border', 'woocommerce-paypal-payments' ), value: 'white-no-border' }, { label: __( 'White (no border)', 'woocommerce-paypal-payments' ), value: 'white-no-border' },
{ label: __( 'Gray', 'woocommerce-paypal-payments' ), value: 'gray' },
{ label: __( 'Monochrome', 'woocommerce-paypal-payments' ), value: 'monochrome' },
{ label: __( 'Grayscale', 'woocommerce-paypal-payments' ), value: 'grayscale' },
]} ]}
value={flexColor} value={flexColor}
onChange={(value) => setAttributes({flexColor: value})} onChange={(value) => setAttributes({flexColor: value})}
@ -154,8 +183,6 @@ export default function Edit( { attributes, clientId, setAttributes } ) {
{ isFlex && (<SelectControl { isFlex && (<SelectControl
label={__('Ratio', 'woocommerce-paypal-payments')} label={__('Ratio', 'woocommerce-paypal-payments')}
options={[ options={[
{ label: __( '1x1', 'woocommerce-paypal-payments' ), value: '1x1' },
{ label: __( '1x4', 'woocommerce-paypal-payments' ), value: '1x4' },
{ label: __( '8x1', 'woocommerce-paypal-payments' ), value: '8x1' }, { label: __( '8x1', 'woocommerce-paypal-payments' ), value: '8x1' },
{ label: __( '20x1', 'woocommerce-paypal-payments' ), value: '20x1' }, { label: __( '20x1', 'woocommerce-paypal-payments' ), value: '20x1' },
]} ]}
@ -167,12 +194,11 @@ export default function Edit( { attributes, clientId, setAttributes } ) {
help={ __( 'Used for the analytics dashboard in the merchant account.', 'woocommerce-paypal-payments' ) } help={ __( 'Used for the analytics dashboard in the merchant account.', 'woocommerce-paypal-payments' ) }
options={ [ options={ [
{ label: __( 'Detect automatically', 'woocommerce-paypal-payments' ), value: 'auto' }, { label: __( 'Detect automatically', 'woocommerce-paypal-payments' ), value: 'auto' },
{ label: __( 'Product Page', 'woocommerce-paypal-payments' ), value: 'product' },
{ label: __( 'Cart', 'woocommerce-paypal-payments' ), value: 'cart' }, { label: __( 'Cart', 'woocommerce-paypal-payments' ), value: 'cart' },
{ label: __( 'Payment', 'woocommerce-paypal-payments' ), value: 'payment' }, { label: __( 'Checkout', 'woocommerce-paypal-payments' ), value: 'checkout' },
{ label: __( 'Product', 'woocommerce-paypal-payments' ), value: 'product' },
{ label: __( 'Product list', 'woocommerce-paypal-payments' ), value: 'product-list' },
{ label: __( 'Home', 'woocommerce-paypal-payments' ), value: 'home' }, { label: __( 'Home', 'woocommerce-paypal-payments' ), value: 'home' },
{ label: __( 'Category', 'woocommerce-paypal-payments' ), value: 'category' }, { label: __( 'Shop', 'woocommerce-paypal-payments' ), value: 'shop' },
] } ] }
value={ placement } value={ placement }
onChange={ ( value ) => setAttributes( { placement: value } ) } onChange={ ( value ) => setAttributes( { placement: value } ) }

View file

@ -1,7 +1,7 @@
import { useBlockProps } from '@wordpress/block-editor'; import { useBlockProps } from '@wordpress/block-editor';
export default function save( { attributes } ) { export default function save( { attributes } ) {
const { layout, logo, position, color, flexColor, flexRatio, placement, id } = attributes; const { layout, logo, position, color, size, flexColor, flexRatio, placement, id } = attributes;
const paypalAttributes = layout === 'flex' ? { const paypalAttributes = layout === 'flex' ? {
'data-pp-style-layout': 'flex', 'data-pp-style-layout': 'flex',
'data-pp-style-color': flexColor, 'data-pp-style-color': flexColor,
@ -11,6 +11,7 @@ export default function save( { attributes } ) {
'data-pp-style-logo-type': logo, 'data-pp-style-logo-type': logo,
'data-pp-style-logo-position': position, 'data-pp-style-logo-position': position,
'data-pp-style-text-color': color, 'data-pp-style-text-color': color,
'data-pp-style-text-size': size,
}; };
if (placement && placement !== 'auto') { if (placement && placement !== 'auto') {
paypalAttributes['data-pp-placement'] = placement; paypalAttributes['data-pp-placement'] = placement;

View file

@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/** /**
@ -22,16 +23,26 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
*/ */
class PayLaterBlockModule implements ModuleInterface { class PayLaterBlockModule implements ModuleInterface {
/** /**
* Returns whether the block should be loaded. * Returns whether the block module should be loaded.
*/ */
public static function is_enabled(): bool { public static function is_module_loading_required(): bool {
return apply_filters( return apply_filters(
// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
'woocommerce.feature-flags.woocommerce_paypal_payments.paylater_block_enabled', 'woocommerce.feature-flags.woocommerce_paypal_payments.paylater_block_enabled',
getenv( 'PCP_PAYLATER_BLOCK' ) === '1' getenv( 'PCP_PAYLATER_BLOCK' ) !== '0'
); );
} }
/**
* Returns whether the block is enabled.
*
* @param SettingsStatus $settings_status The Settings status helper.
* @return bool true if the block is enabled, otherwise false.
*/
public static function is_block_enabled( SettingsStatus $settings_status ): bool {
return self::is_module_loading_required() && $settings_status->is_pay_later_messaging_enabled_for_location( 'custom_placement' );
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -71,13 +82,15 @@ class PayLaterBlockModule implements ModuleInterface {
$script_handle, $script_handle,
'PcpPayLaterBlock', 'PcpPayLaterBlock',
array( array(
'ajax' => array( 'ajax' => array(
'cart_script_params' => array( 'cart_script_params' => array(
'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ),
), ),
), ),
'settingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway' ), 'settingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway' ),
'vaultingEnabled' => $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ), 'vaultingEnabled' => $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ),
'placementEnabled' => self::is_block_enabled( $c->get( 'wcgateway.settings.status' ) ),
'payLaterSettingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&ppcp-tab=ppcp-pay-later' ),
) )
); );

View file

@ -0,0 +1,14 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3.25.0"
}
],
[
"@babel/preset-react"
]
]
}

View file

@ -0,0 +1,3 @@
node_modules
assets/js
assets/css

View file

@ -0,0 +1,17 @@
{
"name": "woocommerce/ppcp-paylater-configurator",
"type": "dhii-mod",
"description": "Pay Later Messaging configurator module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {
"psr-4": {
"WooCommerce\\PayPalCommerce\\PayLaterConfigurator\\": "src"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View file

@ -0,0 +1,83 @@
<?php
/**
* The Pay Later configurator module extensions.
*
* @package WooCommerce\PayPalCommerce\PayLaterConfigurator
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterConfigurator;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
$old_fields = array(
'pay_later_messaging_locations',
'pay_later_enable_styling_per_messaging_location',
'pay_later_general_message_layout',
'pay_later_general_message_logo',
'pay_later_general_message_position',
'pay_later_general_message_color',
'pay_later_general_message_flex_color',
'pay_later_general_message_flex_ratio',
'pay_later_general_message_preview',
'pay_later_product_messaging_heading',
'pay_later_product_message_layout',
'pay_later_product_message_logo',
'pay_later_product_message_position',
'pay_later_product_message_color',
'pay_later_product_message_flex_color',
'pay_later_product_message_flex_ratio',
'pay_later_product_message_preview',
'pay_later_cart_messaging_heading',
'pay_later_cart_message_layout',
'pay_later_cart_message_logo',
'pay_later_cart_message_position',
'pay_later_cart_message_color',
'pay_later_cart_message_flex_color',
'pay_later_cart_message_flex_ratio',
'pay_later_cart_message_preview',
'pay_later_checkout_messaging_heading',
'pay_later_checkout_message_layout',
'pay_later_checkout_message_logo',
'pay_later_checkout_message_position',
'pay_later_checkout_message_color',
'pay_later_checkout_message_flex_color',
'pay_later_checkout_message_flex_ratio',
'pay_later_checkout_message_preview',
'pay_later_shop_messaging_heading',
'pay_later_shop_message_layout',
'pay_later_shop_message_logo',
'pay_later_shop_message_position',
'pay_later_shop_message_color',
'pay_later_shop_message_flex_color',
'pay_later_shop_message_flex_ratio',
'pay_later_shop_message_preview',
'pay_later_home_messaging_heading',
'pay_later_home_message_layout',
'pay_later_home_message_logo',
'pay_later_home_message_position',
'pay_later_home_message_color',
'pay_later_home_message_flex_color',
'pay_later_home_message_flex_ratio',
'pay_later_home_message_preview',
);
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$vault_enabled = $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' );
if ( ! $vault_enabled ) {
$old_fields[] = 'pay_later_messaging_enabled';
}
foreach ( $old_fields as $old_field ) {
unset( $fields[ $old_field ] );
}
return $fields;
},
);

View file

@ -0,0 +1,16 @@
<?php
/**
* The Pay Later configurator module.
*
* @package WooCommerce\PayPalCommerce\PayLaterConfigurator
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterConfigurator;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return new PayLaterConfiguratorModule();
};

View file

@ -0,0 +1,33 @@
{
"name": "ppcp-paylater-configurator",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"browserslist": [
"> 0.5%",
"Safari >= 8",
"Chrome >= 41",
"Firefox >= 43",
"Edge >= 14"
],
"dependencies": {
"core-js": "^3.25.0"
},
"devDependencies": {
"@babel/core": "^7.19",
"@babel/preset-env": "^7.19",
"@babel/preset-react": "^7.18.6",
"@woocommerce/dependency-extraction-webpack-plugin": "^2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"webpack": "^5.76",
"webpack-cli": "^4.10"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
"dev": "cross-env BABEL_ENV=default webpack --watch"
}
}

View file

@ -0,0 +1,55 @@
#messaging-configurator {
& > div {
justify-content: left;
margin: 0;
& > div {
padding: 0;
}
}
.ppcp-paylater-configurator-publishButton {
display:none;
}
.ppcp-paylater-configurator-header {
font-size: 1.3em;
color: #1d2327;
margin: 1em 0;
font-weight: 600;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
line-height: 1.3;
}
#configurator-eligibleContainer div:has(.ppcp-paylater-configurator-header#configurator-headerText) {
display: none;
}
.ppcp-paylater-configurator-subheader {
font-size: 14px;
line-height: 1.5;
color: #3c434a;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
a {
font-size: 14px;
}
}
.css-dpyjrq-text_body, .css-dpyjrq-text_body a {
font-size: 14px;
}
button.css-104jwuk {
background: none;
}
.css-1yo2lxy-text_body_strong, span.css-16jt5za-text_body, span.css-1yo2lxy-text_body_strong, span {
font-size: 14px;
}
hr {
margin-right: 16px;
border-top-color: #B1B7BD;
}
}
#field-pay_later_messaging_heading h3{
margin-bottom: 0px;
}

View file

@ -0,0 +1,62 @@
document.addEventListener( 'DOMContentLoaded', () => {
const form = document.querySelector('#mainform');
const table = form.querySelector('.form-table');
const headingRow = table.querySelector('#field-pay_later_messaging_heading');
const saveChangesButton = form.querySelector('.woocommerce-save-button');
const publishButtonClassName = PcpPayLaterConfigurator.publishButtonClassName;
const tempContainer = document.createElement('div');
tempContainer.innerHTML = `<div id='messaging-configurator'></div>`;
// Get the new row element from the container
const newRow = tempContainer.firstChild;
// Insert the new row after the headingRow
headingRow.parentNode.insertBefore(newRow, headingRow.nextSibling);
let isSaving = false; // Flag variable to track whether saving is in progress
saveChangesButton.addEventListener('click', () => {
// Check if saving is not already in progress
if (!isSaving) {
isSaving = true; // Set flag to indicate saving is in progress
// Trigger the click event on the publish button
form.querySelector('.' + publishButtonClassName).click();
// Trigger click event on saveChangesButton after a short delay
setTimeout(() => {
saveChangesButton.click(); // Trigger click event on saveChangesButton
isSaving = false; // Reset flag when saving is complete
}, 1000); // Adjust the delay as needed
}
});
merchantConfigurators.Messaging({
config: PcpPayLaterConfigurator.config,
merchantClientId: PcpPayLaterConfigurator.merchantClientId,
partnerClientId: PcpPayLaterConfigurator.partnerClientId,
partnerName: 'WooCommerce',
bnCode: 'Woo_PPCP',
placements: ['cart', 'checkout', 'product', 'shop', 'home', 'custom_placement'],
styleOverrides: {
button: publishButtonClassName,
header: PcpPayLaterConfigurator.headerClassName,
subheader: PcpPayLaterConfigurator.subheaderClassName
},
onSave: data => {
fetch(PcpPayLaterConfigurator.ajax.save_config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
nonce: PcpPayLaterConfigurator.ajax.save_config.nonce,
config: data,
}),
});
}
})
} );

View file

@ -0,0 +1,38 @@
<?php
/**
* The Pay Later configurator module services.
*
* @package WooCommerce\PayPalCommerce\PayLaterConfigurator
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterConfigurator;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'paylater-configurator.url' => static function ( ContainerInterface $container ): string {
/**
* Cannot return false for this path.
*
* @psalm-suppress PossiblyFalseArgument
*/
return plugins_url(
'/modules/ppcp-paylater-configurator/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'paylater-configurator.factory.config' => static function ( ContainerInterface $container ): ConfigFactory {
return new ConfigFactory();
},
'paylater-configurator.endpoint.save-config' => static function ( ContainerInterface $container ): SaveConfig {
return new SaveConfig(
$container->get( 'wcgateway.settings' ),
$container->get( 'button.request-data' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -0,0 +1,150 @@
<?php
/**
* The endpoint for saving the Pay Later messaging config from the configurator.
*
* @package WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint;
use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class SaveConfig.
*/
class SaveConfig {
const ENDPOINT = 'ppc-save-message-config';
/**
* The settings.
*
* @var Settings
*/
protected $settings;
/**
* The request data.
*
* @var RequestData
*/
protected $request_data;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* SaveConfig constructor.
*
* @param Settings $settings The settings.
* @param RequestData $request_data The request data.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
Settings $settings,
RequestData $request_data,
LoggerInterface $logger
) {
$this->settings = $settings;
$this->request_data = $request_data;
$this->logger = $logger;
}
/**
* Returns the nonce.
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*/
public function handle_request(): bool {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( 'Not admin.', 403 );
return false;
}
try {
$data = $this->request_data->read_request( $this->nonce() );
$this->save_config( $data['config']['config'] );
wp_send_json_success();
return true;
} catch ( Throwable $error ) {
$this->logger->error( "SaveConfig execution failed. {$error->getMessage()} {$error->getFile()}:{$error->getLine()}" );
wp_send_json_error();
return false;
}
}
/**
* Saves the config into the old settings.
*
* @param array $config The configurator config.
*/
private function save_config( array $config ): void {
$this->settings->set( 'pay_later_enable_styling_per_messaging_location', true );
$this->settings->set( 'pay_later_messaging_enabled', true );
$enabled_locations = array();
foreach ( $config as $placement => $data ) {
$this->save_config_for_location( $data, $placement );
if ( $placement === 'custom_placement' ) {
$data = $data[0] ?? array();
}
if ( $data['status'] === 'enabled' ) {
$enabled_locations[] = $placement;
}
}
$this->settings->set( 'pay_later_messaging_locations', $enabled_locations );
$this->settings->persist();
}
/**
* Saves the config for a location into the old settings.
*
* @param array $config The configurator config for a location.
* @param string $location The location name in the old settings.
*/
private function save_config_for_location( array $config, string $location ): void {
$this->set_value_if_present( $config, 'layout', "pay_later_{$location}_message_layout" );
$this->set_value_if_present( $config, 'color', "pay_later_{$location}_message_flex_color" );
$this->set_value_if_present( $config, 'ratio', "pay_later_{$location}_message_flex_ratio" );
$this->set_value_if_present( $config, 'logo-position', "pay_later_{$location}_message_position" );
$this->set_value_if_present( $config, 'logo-type', "pay_later_{$location}_message_logo" );
$this->set_value_if_present( $config, 'logo-color', "pay_later_{$location}_message_color" );
$this->set_value_if_present( $config, 'text-size', "pay_later_{$location}_message_text_size" );
$this->set_value_if_present( $config, 'text-color', "pay_later_{$location}_message_color" );
}
/**
* Sets the value in the settings if it is available in the config.
*
* @param array $config The configurator config.
* @param string $key The key in the config.
* @param string $settings_key The key in the settings.
*/
private function set_value_if_present( array $config, string $key, string $settings_key ): void {
if ( isset( $config[ $key ] ) ) {
$this->settings->set( $settings_key, $config[ $key ] );
}
}
}

View file

@ -0,0 +1,145 @@
<?php
/**
* The factory the Pay Later messaging configurator configs.
*
* @package WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class ConfigFactory.
*/
class ConfigFactory {
/**
* Returns the configurator config for the given old settings.
*
* @param Settings $settings The settings.
*/
public function from_settings( Settings $settings ): array {
return array(
'cart' => $this->for_location( $settings, 'cart' ),
'checkout' => $this->for_location( $settings, 'checkout' ),
'product' => $this->for_location( $settings, 'product' ),
'shop' => $this->for_location( $settings, 'shop' ),
'home' => $this->for_location( $settings, 'home' ),
'custom_placement' => array( $this->for_location( $settings, 'woocommerceBlock' ) ),
);
}
/**
* Returns the configurator config for a location.
*
* @param Settings $settings The settings.
* @param string $location The location name in the old settings.
*/
private function for_location( Settings $settings, string $location ): array {
$selected_locations = $settings->has( 'pay_later_messaging_locations' ) ? $settings->get( 'pay_later_messaging_locations' ) : array();
switch ( $location ) {
case 'shop':
case 'home':
$config = $this->for_shop_or_home( $settings, $location, $selected_locations );
break;
case 'woocommerceBlock':
$config = $this->for_woocommerce_block( $selected_locations );
break;
default:
$config = $this->for_default_location( $settings, $location, $selected_locations );
break;
}
return $config;
}
/**
* Returns the configurator config for shop, home locations.
*
* @param Settings $settings The settings.
* @param string $location The location.
* @param string[] $selected_locations The list of selected locations.
* @return array{
* layout: string,
* color: string,
* ratio: string,
* status: "disabled"|"enabled",
* placement: string
* } The configurator config map.
*/
private function for_shop_or_home( Settings $settings, string $location, array $selected_locations ): array {
return array(
'layout' => $this->get_or_default( $settings, "pay_later_{$location}_message_layout", 'flex' ),
'color' => $this->get_or_default( $settings, "pay_later_{$location}_message_flex_color", 'black' ),
'ratio' => $this->get_or_default( $settings, "pay_later_{$location}_message_flex_ratio", '8x1' ),
'status' => in_array( $location, $selected_locations, true ) ? 'enabled' : 'disabled',
'placement' => $location,
);
}
/**
* Returns the configurator config for woocommerceBlock location.
*
* @param array $selected_locations The list of selected locations.
* @return array{
* status: "disabled"|"enabled",
* message_reference: string
* } The configurator config map.
*/
private function for_woocommerce_block( array $selected_locations ): array {
return array(
'status' => in_array( 'custom_placement', $selected_locations, true ) ? 'enabled' : 'disabled',
'message_reference' => 'woocommerceBlock',
);
}
/**
* Returns the configurator config for default locations.
*
* @param Settings $settings The settings.
* @param string $location The location.
* @param string[] $selected_locations The list of selected locations.
* @return array{
* layout: string,
* logo-position: string,
* logo-type: string,
* text-color: string,
* text-size: string,
* status: "disabled"|"enabled",
* placement: string
* } The configurator config map.
*/
private function for_default_location( Settings $settings, string $location, array $selected_locations ): array {
return array(
'layout' => $this->get_or_default( $settings, "pay_later_{$location}_message_layout", 'text' ),
'logo-position' => $this->get_or_default( $settings, "pay_later_{$location}_message_position", 'left' ),
'logo-type' => $this->get_or_default( $settings, "pay_later_{$location}_message_logo", 'inline' ),
'text-color' => $this->get_or_default( $settings, "pay_later_{$location}_message_color", 'black' ),
'text-size' => $this->get_or_default( $settings, "pay_later_{$location}_message_text_size", '12' ),
'status' => in_array( $location, $selected_locations, true ) ? 'enabled' : 'disabled',
'placement' => $location,
);
}
/**
* Returns the settings value or default, if does not exist or not allowed value.
*
* @param Settings $settings The settings.
* @param string $key The key.
* @param mixed $default The default value.
* @param array|null $allowed_values The list of allowed values, or null if all values are allowed.
* @return string
*/
private function get_or_default( Settings $settings, string $key, $default, ?array $allowed_values = null ): string {
if ( $settings->has( $key ) ) {
$value = $settings->get( $key );
if ( ! $allowed_values || in_array( $value, $allowed_values, true ) ) {
return $value;
}
}
return $default;
}
}

View file

@ -0,0 +1,135 @@
<?php
/**
* The Pay Later configurator module.
*
* @package WooCommerce\PayPalCommerce\PayLaterConfigurator
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterConfigurator;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class PayLaterConfiguratorModule
*/
class PayLaterConfiguratorModule implements ModuleInterface {
/**
* Returns whether the module should be loaded.
*/
public static function is_enabled(): bool {
return apply_filters(
// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
'woocommerce.feature-flags.woocommerce_paypal_payments.paylater_configurator_enabled',
getenv( 'PCP_PAYLATER_CONFIGURATOR' ) !== '0'
);
}
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
$messages_apply = $c->get( 'button.helper.messages-apply' );
assert( $messages_apply instanceof MessagesApply );
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$vault_enabled = $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' );
if ( $vault_enabled || ! $messages_apply->for_country() ) {
return;
}
add_action(
'wc_ajax_' . SaveConfig::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'paylater-configurator.endpoint.save-config' );
assert( $endpoint instanceof SaveConfig );
$endpoint->handle_request();
}
);
$current_page_id = $c->get( 'wcgateway.current-ppcp-settings-page-id' );
if ( $current_page_id !== Settings::PAY_LATER_TAB_ID ) {
return;
}
add_action(
'init',
static function () use ( $c, $settings ) {
wp_enqueue_script(
'ppcp-paylater-configurator-lib',
'https://www.paypalobjects.com/merchant-library/merchant-configurator.js',
array(),
$c->get( 'ppcp.asset-version' ),
true
);
wp_enqueue_script(
'ppcp-paylater-configurator',
$c->get( 'paylater-configurator.url' ) . '/assets/js/paylater-configurator.js',
array(),
$c->get( 'ppcp.asset-version' ),
true
);
wp_enqueue_style(
'ppcp-paylater-configurator-style',
$c->get( 'paylater-configurator.url' ) . '/assets/css/paylater-configurator.css',
array(),
$c->get( 'ppcp.asset-version' )
);
$config_factory = $c->get( 'paylater-configurator.factory.config' );
assert( $config_factory instanceof ConfigFactory );
wp_localize_script(
'ppcp-paylater-configurator',
'PcpPayLaterConfigurator',
array(
'ajax' => array(
'save_config' => array(
'endpoint' => \WC_AJAX::get_endpoint( SaveConfig::ENDPOINT ),
'nonce' => wp_create_nonce( SaveConfig::nonce() ),
),
),
'config' => $config_factory->from_settings( $settings ),
'merchantClientId' => $settings->get( 'client_id' ),
'partnerClientId' => $c->get( 'api.partner_merchant_id' ),
'publishButtonClassName' => 'ppcp-paylater-configurator-publishButton',
'headerClassName' => 'ppcp-paylater-configurator-header',
'subheaderClassName' => 'ppcp-paylater-configurator-subheader',
)
);
}
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
}
}

View file

@ -0,0 +1,39 @@
const path = require('path');
const isProduction = process.env.NODE_ENV === 'production';
const DependencyExtractionWebpackPlugin = require( '@woocommerce/dependency-extraction-webpack-plugin' );
module.exports = {
devtool: isProduction ? 'source-map' : 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
plugins: [ new DependencyExtractionWebpackPlugin() ],
entry: {
'paylater-configurator': path.resolve('./resources/js/paylater-configurator.js'),
'paylater-configurator-style': path.resolve('./resources/css/paylater-configurator.scss'),
},
output: {
path: path.resolve(__dirname, 'assets/'),
filename: 'js/[name].js',
},
module: {
rules: [{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
{
loader: 'file-loader',
options: {
name: 'css/[name].css',
}
},
{loader:'sass-loader'}
]
}]
}
};

File diff suppressed because it is too large Load diff

View file

@ -196,7 +196,7 @@ class PayPalSubscriptionsModule implements ModuleInterface {
* *
* @psalm-suppress MissingClosureParamType * @psalm-suppress MissingClosureParamType
*/ */
function( $actions, $subscription ): array { function( $actions, $subscription = null ): array {
if ( ! is_array( $actions ) || ! is_a( $subscription, WC_Subscription::class ) ) { if ( ! is_array( $actions ) || ! is_a( $subscription, WC_Subscription::class ) ) {
return $actions; return $actions;
} }

View file

@ -114,7 +114,7 @@ class SubscriptionsApiHandler {
*/ */
public function create_product( WC_Product $product ) { public function create_product( WC_Product $product ) {
try { try {
$subscription_product = $this->products_endpoint->create( $product->get_title(), $this->prepare_description( $product->get_description() ) ); $subscription_product = $this->products_endpoint->create( $this->prepare_item_string( $product->get_title() ), $this->prepare_item_string( $product->get_description() ) );
$product->update_meta_data( 'ppcp_subscription_product', $subscription_product->to_array() ); $product->update_meta_data( 'ppcp_subscription_product', $subscription_product->to_array() );
$product->save(); $product->save();
} catch ( RuntimeException $exception ) { } catch ( RuntimeException $exception ) {
@ -169,7 +169,7 @@ class SubscriptionsApiHandler {
$catalog_product_name = $catalog_product->name() ?: ''; $catalog_product_name = $catalog_product->name() ?: '';
$catalog_product_description = $catalog_product->description() ?: ''; $catalog_product_description = $catalog_product->description() ?: '';
$wc_product_description = $this->prepare_description( $product->get_description() ) ?: $product->get_title(); $wc_product_description = $this->prepare_item_string( $product->get_description() ) ?: $this->prepare_item_string( $product->get_title() );
if ( $catalog_product_name !== $product->get_title() || $catalog_product_description !== $wc_product_description ) { if ( $catalog_product_name !== $product->get_title() || $catalog_product_description !== $wc_product_description ) {
$data = array(); $data = array();

View file

@ -12,7 +12,7 @@ import ErrorHandler from "../../../ppcp-button/resources/js/modules/ErrorHandler
import {cardFieldStyles} from "../../../ppcp-button/resources/js/modules/Helper/CardFieldsHelper"; import {cardFieldStyles} from "../../../ppcp-button/resources/js/modules/Helper/CardFieldsHelper";
const errorHandler = new ErrorHandler( const errorHandler = new ErrorHandler(
PayPalCommerceGateway.labels.error.generic, ppcp_add_payment_method.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper') document.querySelector('.woocommerce-notices-wrapper')
); );

View file

@ -9,9 +9,9 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods; namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Helper\SavePaymentMethodsApplies; use WooCommerce\PayPalCommerce\SavePaymentMethods\Helper\SavePaymentMethodsApplies;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
@ -57,6 +57,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'AT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'BE' => array( 'BE' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -345,6 +369,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'IE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'IT' => array( 'IT' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -393,6 +441,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'LI' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'LT' => array( 'LT' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -465,6 +537,30 @@ return array(
'TWD', 'TWD',
'USD', 'USD',
), ),
'NO' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'NL' => array( 'NL' => array(
'AUD', 'AUD',
'BRL', 'BRL',
@ -709,30 +805,17 @@ return array(
$container->get( 'api.endpoint.payment-method-tokens' ) $container->get( 'api.endpoint.payment-method-tokens' )
); );
}, },
'save-payment-methods.wc-payment-tokens' => static function( ContainerInterface $container ): WooCommercePaymentTokens {
return new WooCommercePaymentTokens(
$container->get( 'vaulting.payment-token-helper' ),
$container->get( 'vaulting.payment-token-factory' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'save-payment-methods.endpoint.create-payment-token' => static function ( ContainerInterface $container ): CreatePaymentToken { 'save-payment-methods.endpoint.create-payment-token' => static function ( ContainerInterface $container ): CreatePaymentToken {
return new CreatePaymentToken( return new CreatePaymentToken(
$container->get( 'button.request-data' ), $container->get( 'button.request-data' ),
$container->get( 'api.endpoint.payment-method-tokens' ), $container->get( 'api.endpoint.payment-method-tokens' ),
$container->get( 'save-payment-methods.wc-payment-tokens' ) $container->get( 'vaulting.wc-payment-tokens' )
); );
}, },
'save-payment-methods.endpoint.capture-card-payment' => static function( ContainerInterface $container ): CaptureCardPayment { 'save-payment-methods.endpoint.create-payment-token-for-guest' => static function ( ContainerInterface $container ): CreatePaymentTokenForGuest {
return new CaptureCardPayment( return new CreatePaymentTokenForGuest(
$container->get( 'button.request-data' ), $container->get( 'button.request-data' ),
$container->get( 'api.host' ), $container->get( 'api.endpoint.payment-method-tokens' )
$container->get( 'api.bearer' ),
$container->get( 'api.factory.order' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'session.handler' ),
$container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
); );

View file

@ -1,219 +0,0 @@
<?php
/**
* The Capture Card Payment endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint;
use Psr\Log\LoggerInterface;
use RuntimeException;
use stdClass;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WP_Error;
/**
* Class CaptureCardPayment
*/
class CaptureCardPayment implements EndpointInterface {
use RequestTrait;
const ENDPOINT = 'ppc-capture-card-payment';
/**
* The request data.
*
* @var RequestData
*/
private $request_data;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The order factory.
*
* @var OrderFactory
*/
private $order_factory;
/**
* The purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The session handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* CaptureCardPayment constructor.
*
* @param RequestData $request_data The request data.
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param OrderFactory $order_factory The order factory.
* @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
string $host,
Bearer $bearer,
OrderFactory $order_factory,
PurchaseUnitFactory $purchase_unit_factory,
OrderEndpoint $order_endpoint,
SessionHandler $session_handler,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->host = $host;
$this->bearer = $bearer;
$this->order_factory = $order_factory;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->order_endpoint = $order_endpoint;
$this->logger = $logger;
$this->session_handler = $session_handler;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
$data = $this->request_data->read_request( $this->nonce() );
$tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id() );
foreach ( $tokens as $token ) {
if ( $token->get_id() === (int) $data['payment_token'] ) {
try {
$order = $this->create_order( $token->get_token() );
$id = $order->id ?? '';
$status = $order->status ?? '';
$payment_source = isset( $order->payment_source->card ) ? 'card' : '';
if ( $id && $status && $payment_source ) {
WC()->session->set(
'ppcp_saved_payment_card',
array(
'order_id' => $id,
'status' => $status,
'payment_source' => $payment_source,
)
);
wp_send_json_success();
return true;
}
} catch ( RuntimeException $exception ) {
wp_send_json_error();
return false;
}
}
}
wp_send_json_error();
return false;
}
/**
* Creates PayPal order from the given card vault id.
*
* @param string $vault_id Vault id.
* @return stdClass
* @throws RuntimeException When request fails.
*/
private function create_order( string $vault_id ): stdClass {
$items = array( $this->purchase_unit_factory->from_wc_cart() );
$data = array(
'intent' => 'CAPTURE',
'purchase_units' => array_map(
static function ( PurchaseUnit $item ): array {
return $item->to_array( true, false );
},
$items
),
'payment_source' => array(
'card' => array(
'vault_id' => $vault_id,
),
),
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
return json_decode( $response['body'] );
}
}

View file

@ -14,9 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface; use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData; use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\SavePaymentMethods\WooCommercePaymentTokens; use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/** /**
* Class CreatePaymentToken * Class CreatePaymentToken
@ -96,7 +94,7 @@ class CreatePaymentToken implements EndpointInterface {
) )
); );
$result = $this->payment_method_tokens_endpoint->payment_tokens( $payment_source ); $result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source );
if ( is_user_logged_in() && isset( $result->customer->id ) ) { if ( is_user_logged_in() && isset( $result->customer->id ) ) {
$current_user_id = get_current_user_id(); $current_user_id = get_current_user_id();

View file

@ -0,0 +1,90 @@
<?php
/**
* Create payment token for guest user.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint;
use Exception;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
/**
* Class UpdateCustomerId
*/
class CreatePaymentTokenForGuest implements EndpointInterface {
const ENDPOINT = 'ppc-update-customer-id';
/**
* The request data.
*
* @var RequestData
*/
private $request_data;
/**
* The payment method tokens endpoint.
*
* @var PaymentMethodTokensEndpoint
*/
private $payment_method_tokens_endpoint;
/**
* CreatePaymentToken constructor.
*
* @param RequestData $request_data The request data.
* @param PaymentMethodTokensEndpoint $payment_method_tokens_endpoint The payment method tokens endpoint.
*/
public function __construct(
RequestData $request_data,
PaymentMethodTokensEndpoint $payment_method_tokens_endpoint
) {
$this->request_data = $request_data;
$this->payment_method_tokens_endpoint = $payment_method_tokens_endpoint;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
* @throws Exception On Error.
*/
public function handle_request(): bool {
$data = $this->request_data->read_request( $this->nonce() );
/**
* Suppress ArgumentTypeCoercion
*
* @psalm-suppress ArgumentTypeCoercion
*/
$payment_source = new PaymentSource(
'token',
(object) array(
'id' => $data['vault_setup_token'],
'type' => 'SETUP_TOKEN',
)
);
$result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source );
WC()->session->set( 'ppcp_guest_payment_for_free_trial', $result );
wp_send_json_success();
return true;
}
}

View file

@ -19,9 +19,10 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
@ -89,14 +90,7 @@ class SavePaymentMethodsModule implements ModuleInterface {
$logger = $c->get( 'woocommerce.logger.woocommerce' ); $logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface ); assert( $logger instanceof LoggerInterface );
$localized_script_data = $this->add_id_token_to_script_data( $api, $logger, $localized_script_data ); return $this->add_id_token_to_script_data( $api, $logger, $localized_script_data );
$localized_script_data['ajax']['capture_card_payment'] = array(
'endpoint' => \WC_AJAX::get_endpoint( CaptureCardPayment::ENDPOINT ),
'nonce' => wp_create_nonce( CaptureCardPayment::nonce() ),
);
return $localized_script_data;
} }
); );
@ -148,6 +142,21 @@ class SavePaymentMethodsModule implements ModuleInterface {
'vault' => array( 'vault' => array(
'store_in_vault' => 'ON_SUCCESS', 'store_in_vault' => 'ON_SUCCESS',
'usage_type' => 'MERCHANT', 'usage_type' => 'MERCHANT',
'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ),
),
),
),
);
} elseif ( $funding_source && $funding_source === 'apple_pay' ) {
$data['payment_source'] = array(
'apple_pay' => array(
'stored_credential' => array(
'payment_initiator' => 'CUSTOMER',
'payment_type' => 'RECURRING',
),
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
), ),
), ),
), ),
@ -159,6 +168,7 @@ class SavePaymentMethodsModule implements ModuleInterface {
'vault' => array( 'vault' => array(
'store_in_vault' => 'ON_SUCCESS', 'store_in_vault' => 'ON_SUCCESS',
'usage_type' => 'MERCHANT', 'usage_type' => 'MERCHANT',
'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ),
), ),
), ),
), ),
@ -188,7 +198,7 @@ class SavePaymentMethodsModule implements ModuleInterface {
update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id ); update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id );
$wc_payment_tokens = $c->get( 'save-payment-methods.wc-payment-tokens' ); $wc_payment_tokens = $c->get( 'vaulting.wc-payment-tokens' );
assert( $wc_payment_tokens instanceof WooCommercePaymentTokens ); assert( $wc_payment_tokens instanceof WooCommercePaymentTokens );
if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) {
@ -207,11 +217,29 @@ class SavePaymentMethodsModule implements ModuleInterface {
} }
if ( $wc_order->get_payment_method() === PayPalGateway::ID ) { if ( $wc_order->get_payment_method() === PayPalGateway::ID ) {
$wc_payment_tokens->create_payment_token_paypal( switch ( $payment_source->name() ) {
$wc_order->get_customer_id(), case 'venmo':
$token_id, $wc_payment_tokens->create_payment_token_venmo(
$payment_source->properties()->email_address ?? '' $wc_order->get_customer_id(),
); $token_id,
$payment_source->properties()->email_address ?? ''
);
break;
case 'apple_pay':
$wc_payment_tokens->create_payment_token_applepay(
$wc_order->get_customer_id(),
$token_id
);
break;
case 'paypal':
default:
$wc_payment_tokens->create_payment_token_paypal(
$wc_order->get_customer_id(),
$token_id,
$payment_source->properties()->email_address ?? ''
);
break;
}
} }
} }
}, },
@ -254,7 +282,11 @@ class SavePaymentMethodsModule implements ModuleInterface {
$settings = $c->get( 'wcgateway.settings' ); $settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings ); assert( $settings instanceof Settings );
$verification_method = $settings->has( '3d_secure_contingency' ) ? $settings->get( '3d_secure_contingency' ) : '';
$verification_method =
$settings->has( '3d_secure_contingency' )
? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) )
: '';
$change_payment_method = wc_clean( wp_unslash( $_GET['change_payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification $change_payment_method = wc_clean( wp_unslash( $_GET['change_payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification
@ -285,6 +317,14 @@ class SavePaymentMethodsModule implements ModuleInterface {
'nonce' => wp_create_nonce( SubscriptionChangePaymentMethod::nonce() ), 'nonce' => wp_create_nonce( SubscriptionChangePaymentMethod::nonce() ),
), ),
), ),
'labels' => array(
'error' => array(
'generic' => __(
'Something went wrong. Please try again or choose another payment source.',
'woocommerce-paypal-payments'
),
),
),
) )
); );
} catch ( RuntimeException $exception ) { } catch ( RuntimeException $exception ) {
@ -332,6 +372,16 @@ class SavePaymentMethodsModule implements ModuleInterface {
} }
); );
add_action(
'wc_ajax_' . CreatePaymentTokenForGuest::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token-for-guest' );
assert( $endpoint instanceof CreatePaymentTokenForGuest );
$endpoint->handle_request();
}
);
add_action( add_action(
'woocommerce_paypal_payments_before_delete_payment_token', 'woocommerce_paypal_payments_before_delete_payment_token',
function( string $token_id ) use ( $c ) { function( string $token_id ) use ( $c ) {
@ -368,16 +418,6 @@ class SavePaymentMethodsModule implements ModuleInterface {
} }
); );
add_action(
'wc_ajax_' . CaptureCardPayment::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'save-payment-methods.endpoint.capture-card-payment' );
assert( $endpoint instanceof CaptureCardPayment );
$endpoint->handle_request();
}
);
add_filter( add_filter(
'woocommerce_paypal_payments_save_payment_methods_eligible', 'woocommerce_paypal_payments_save_payment_methods_eligible',
function() { function() {

View file

@ -1,148 +0,0 @@
<?php
/**
* Service to create WC Payment Tokens.
*
* @package WooCommerce\PayPalCommerce\Applepay
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use Exception;
use Psr\Log\LoggerInterface;
use stdClass;
use WC_Payment_Token_CC;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenFactory;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenPayPal;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class WooCommercePaymentTokens
*/
class WooCommercePaymentTokens {
/**
* The payment token helper.
*
* @var PaymentTokenHelper
*/
private $payment_token_helper;
/**
* The payment token factory.
*
* @var PaymentTokenFactory
*/
private $payment_token_factory;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* WooCommercePaymentTokens constructor.
*
* @param PaymentTokenHelper $payment_token_helper The payment token helper.
* @param PaymentTokenFactory $payment_token_factory The payment token factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
PaymentTokenHelper $payment_token_helper,
PaymentTokenFactory $payment_token_factory,
LoggerInterface $logger
) {
$this->payment_token_helper = $payment_token_helper;
$this->payment_token_factory = $payment_token_factory;
$this->logger = $logger;
}
/**
* Creates a WC Payment Token for PayPal payment.
*
* @param int $customer_id The WC customer ID.
* @param string $token The PayPal payment token.
* @param string $email The PayPal customer email.
*
* @return int
*/
public function create_payment_token_paypal(
int $customer_id,
string $token,
string $email
): int {
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID );
if ( $this->payment_token_helper->token_exist( $wc_tokens, $token ) ) {
return 0;
}
$payment_token_paypal = $this->payment_token_factory->create( 'paypal' );
assert( $payment_token_paypal instanceof PaymentTokenPayPal );
$payment_token_paypal->set_token( $token );
$payment_token_paypal->set_user_id( $customer_id );
$payment_token_paypal->set_gateway_id( PayPalGateway::ID );
if ( $email && is_email( $email ) ) {
$payment_token_paypal->set_email( $email );
}
try {
$payment_token_paypal->save();
} catch ( Exception $exception ) {
$this->logger->error(
"Could not create WC payment token PayPal for customer {$customer_id}. " . $exception->getMessage()
);
}
return $payment_token_paypal->get_id();
}
/**
* Creates a WC Payment Token for Credit Card payment.
*
* @param int $customer_id The WC customer ID.
* @param stdClass $payment_token The Credit Card payment token.
*
* @return int
*/
public function create_payment_token_card( int $customer_id, stdClass $payment_token ): int {
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, CreditCardGateway::ID );
if ( $this->payment_token_helper->token_exist( $wc_tokens, $payment_token->id ) ) {
return 0;
}
$token = new WC_Payment_Token_CC();
$token->set_token( $payment_token->id );
$token->set_user_id( get_current_user_id() );
$token->set_gateway_id( CreditCardGateway::ID );
$token->set_last4( $payment_token->payment_source->card->last_digits ?? '' );
$expiry = explode( '-', $payment_token->payment_source->card->expiry ?? '' );
$token->set_expiry_year( $expiry[0] ?? '' );
$token->set_expiry_month( $expiry[1] ?? '' );
$brand = $payment_token->payment_source->card->brand ?? __( 'N/A', 'woocommerce-paypal-payments' );
if ( $brand ) {
$token->set_card_type( $brand );
}
try {
$token->save();
} catch ( Exception $exception ) {
$this->logger->error(
"Could not create WC payment token card for customer {$customer_id}. " . $exception->getMessage()
);
}
$token->save();
return $token->get_id();
}
}

View file

@ -126,6 +126,10 @@ class PaymentTokenChecker {
return; return;
} }
if ( ! in_array( $wc_order->get_payment_method(), array( PayPalGateway::ID, CreditCardGateway::ID, CardButtonGateway::ID ), true ) ) {
return;
}
if ( $wc_order->get_status() === 'processing' || 'capture' !== $intent ) { if ( $wc_order->get_status() === 'processing' || 'capture' !== $intent ) {
return; return;
} }

View file

@ -78,6 +78,8 @@ class StatusReportModule implements ModuleInterface {
$had_ppec_plugin = PPECHelper::is_plugin_configured(); $had_ppec_plugin = PPECHelper::is_plugin_configured();
$subscription_mode_options = $c->get( 'wcgateway.settings.fields.subscriptions_mode_options' );
$items = array( $items = array(
array( array(
'label' => esc_html__( 'Onboarded', 'woocommerce-paypal-payments' ), 'label' => esc_html__( 'Onboarded', 'woocommerce-paypal-payments' ),
@ -169,7 +171,7 @@ class StatusReportModule implements ModuleInterface {
'description' => esc_html__( 'Whether subscriptions are active and their mode.', 'woocommerce-paypal-payments' ), 'description' => esc_html__( 'Whether subscriptions are active and their mode.', 'woocommerce-paypal-payments' ),
'value' => $this->subscriptions_mode_text( 'value' => $this->subscriptions_mode_text(
$subscription_helper->plugin_is_active(), $subscription_helper->plugin_is_active(),
$settings->has( 'subscriptions_mode' ) ? (string) $settings->get( 'subscriptions_mode' ) : '', $settings->has( 'subscriptions_mode' ) ? (string) $subscription_mode_options[ $settings->get( 'subscriptions_mode' ) ] : '',
$subscriptions_mode_settings $subscriptions_mode_settings
), ),
), ),

View file

@ -56,4 +56,15 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
'vaulting.wc-payment-tokens' => static function( ContainerInterface $container ): WooCommercePaymentTokens {
return new WooCommercePaymentTokens(
$container->get( 'vaulting.payment-token-helper' ),
$container->get( 'vaulting.payment-token-factory' ),
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'vaulting.vault-v3-enabled' => static function( ContainerInterface $container ): bool {
return $container->has( 'save-payment-methods.eligible' ) && $container->get( 'save-payment-methods.eligible' );
},
); );

View file

@ -0,0 +1,31 @@
<?php
/**
* WooCommerce Payment token for ApplePay.
*
* @package WooCommerce\PayPalCommerce\Vaulting
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
use WC_Payment_Token;
/**
* Class PaymentTokenApplePay
*/
class PaymentTokenApplePay extends WC_Payment_Token {
/**
* Token Type String.
*
* @var string
*/
protected $type = 'ApplePay';
/**
* Extra data.
*
* @var string[]
*/
protected $extra_data = array();
}

View file

@ -19,12 +19,16 @@ class PaymentTokenFactory {
* *
* @param string $type The type of WC payment token. * @param string $type The type of WC payment token.
* *
* @return void|PaymentTokenPayPal * @return void|PaymentTokenPayPal|PaymentTokenVenmo|PaymentTokenApplePay
*/ */
public function create( string $type ) { public function create( string $type ) {
switch ( $type ) { switch ( $type ) {
case 'paypal': case 'paypal':
return new PaymentTokenPayPal(); return new PaymentTokenPayPal();
case 'venmo':
return new PaymentTokenVenmo();
case 'apple_pay':
return new PaymentTokenApplePay();
} }
} }
} }

View file

@ -21,15 +21,39 @@ class PaymentTokenHelper {
* *
* @param WC_Payment_Token[] $wc_tokens WC Payment Tokens. * @param WC_Payment_Token[] $wc_tokens WC Payment Tokens.
* @param string $token_id Payment Token ID. * @param string $token_id Payment Token ID.
* @param ?string $class_name Class name of the token.
* @return bool * @return bool
*/ */
public function token_exist( array $wc_tokens, string $token_id ): bool { public function token_exist( array $wc_tokens, string $token_id, string $class_name = null ): bool {
foreach ( $wc_tokens as $wc_token ) { foreach ( $wc_tokens as $wc_token ) {
if ( $wc_token->get_token() === $token_id ) { if ( $wc_token->get_token() === $token_id ) {
return true; if ( null !== $class_name ) {
if ( $wc_token instanceof $class_name ) {
return true;
}
} else {
return true;
}
} }
} }
return false; return false;
} }
/**
* Checks if given token exist as WC Payment Token.
*
* @param array $wc_tokens WC Payment Tokens.
* @param string $class_name Class name of the token.
* @return null|WC_Payment_Token
*/
public function first_token_of_type( array $wc_tokens, string $class_name ) {
foreach ( $wc_tokens as $wc_token ) {
if ( $wc_token instanceof $class_name ) {
return $wc_token;
}
}
return null;
}
} }

View file

@ -0,0 +1,51 @@
<?php
/**
* WooCommerce Payment token for Venmo.
*
* @package WooCommerce\PayPalCommerce\Vaulting
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
use WC_Payment_Token;
/**
* Class PaymentTokenVenmo
*/
class PaymentTokenVenmo extends WC_Payment_Token {
/**
* Token Type String.
*
* @var string
*/
protected $type = 'Venmo';
/**
* Extra data.
*
* @var string[]
*/
protected $extra_data = array(
'email' => '',
);
/**
* Get PayPal account email.
*
* @return string PayPal account email.
*/
public function get_email() {
return $this->get_meta( 'email' );
}
/**
* Set PayPal account email.
*
* @param string $email PayPal account email.
*/
public function set_email( $email ) {
$this->add_meta_data( 'email', $email, true );
}
}

View file

@ -107,7 +107,7 @@ class PaymentTokensMigration {
} }
} elseif ( $token->source()->paypal ) { } elseif ( $token->source()->paypal ) {
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $id, PayPalGateway::ID ); $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $id, PayPalGateway::ID );
if ( $this->payment_token_helper->token_exist( $wc_tokens, $token->id() ) ) { if ( $this->payment_token_helper->token_exist( $wc_tokens, $token->id(), PaymentTokenPayPal::class ) ) {
$this->logger->info( 'Token already exist for user ' . (string) $id ); $this->logger->info( 'Token already exist for user ' . (string) $id );
continue; continue;
} }

View file

@ -81,11 +81,50 @@ class VaultingModule implements ModuleInterface {
if ( $type === 'WC_Payment_Token_PayPal' ) { if ( $type === 'WC_Payment_Token_PayPal' ) {
return PaymentTokenPayPal::class; return PaymentTokenPayPal::class;
} }
if ( $type === 'WC_Payment_Token_Venmo' ) {
return PaymentTokenVenmo::class;
}
if ( $type === 'WC_Payment_Token_ApplePay' ) {
return PaymentTokenApplePay::class;
}
return $type; return $type;
} }
); );
add_filter(
'woocommerce_get_customer_payment_tokens',
/**
* Filter available payment tokens depending on context.
*
* @psalm-suppress MissingClosureParamType
* @psalm-suppress MissingClosureReturnType
*/
function( $tokens, $customer_id, $gateway_id ) {
if ( ! is_array( $tokens ) ) {
return $tokens;
}
$is_post = isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] === 'POST';
// Exclude ApplePay tokens from payment pages.
if (
( is_checkout() || is_cart() || is_product() )
&& ! $is_post // Don't check on POST so we have all payment methods on form submissions.
) {
foreach ( $tokens as $index => $token ) {
if ( $token instanceof PaymentTokenApplePay ) {
unset( $tokens[ $index ] );
}
}
}
return $tokens;
},
10,
3
);
add_filter( add_filter(
'woocommerce_payment_methods_list_item', 'woocommerce_payment_methods_list_item',
/** /**
@ -98,10 +137,18 @@ class VaultingModule implements ModuleInterface {
return $item; return $item;
} }
if ( strtolower( $payment_token->get_type() ) === 'paypal' ) { if ( $payment_token instanceof PaymentTokenPayPal ) {
assert( $payment_token instanceof PaymentTokenPayPal ); $item['method']['brand'] = 'PayPal / ' . $payment_token->get_email();
$item['method']['brand'] = $payment_token->get_email(); return $item;
}
if ( $payment_token instanceof PaymentTokenVenmo ) {
$item['method']['brand'] = 'Venmo / ' . $payment_token->get_email();
return $item;
}
if ( $payment_token instanceof PaymentTokenApplePay ) {
$item['method']['brand'] = 'ApplePay #' . ( (string) $payment_token->get_id() );
return $item; return $item;
} }

View file

@ -0,0 +1,326 @@
<?php
/**
* Service to create WC Payment Tokens.
*
* @package WooCommerce\PayPalCommerce\Applepay
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
use Exception;
use Psr\Log\LoggerInterface;
use stdClass;
use WC_Payment_Token_CC;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class WooCommercePaymentTokens
*/
class WooCommercePaymentTokens {
/**
* The payment token helper.
*
* @var PaymentTokenHelper
*/
private $payment_token_helper;
/**
* The payment token factory.
*
* @var PaymentTokenFactory
*/
private $payment_token_factory;
/**
* Payment tokens endpoint.
*
* @var PaymentTokensEndpoint
*/
private $payment_tokens_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* WooCommercePaymentTokens constructor.
*
* @param PaymentTokenHelper $payment_token_helper The payment token helper.
* @param PaymentTokenFactory $payment_token_factory The payment token factory.
* @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
PaymentTokenHelper $payment_token_helper,
PaymentTokenFactory $payment_token_factory,
PaymentTokensEndpoint $payment_tokens_endpoint,
LoggerInterface $logger
) {
$this->payment_token_helper = $payment_token_helper;
$this->payment_token_factory = $payment_token_factory;
$this->payment_tokens_endpoint = $payment_tokens_endpoint;
$this->logger = $logger;
}
/**
* Creates a WC Payment Token for PayPal payment.
*
* @param int $customer_id The WC customer ID.
* @param string $token The PayPal payment token.
* @param string $email The PayPal customer email.
*
* @return int
*/
public function create_payment_token_paypal(
int $customer_id,
string $token,
string $email
): int {
if ( $customer_id === 0 ) {
return 0;
}
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID );
if ( $this->payment_token_helper->token_exist( $wc_tokens, $token, PaymentTokenPayPal::class ) ) {
return 0;
}
// Try to update existing token of type before creating a new one.
$payment_token_paypal = $this->payment_token_helper->first_token_of_type( $wc_tokens, PaymentTokenPayPal::class );
if ( ! $payment_token_paypal ) {
$payment_token_paypal = $this->payment_token_factory->create( 'paypal' );
}
assert( $payment_token_paypal instanceof PaymentTokenPayPal );
$payment_token_paypal->set_token( $token );
$payment_token_paypal->set_user_id( $customer_id );
$payment_token_paypal->set_gateway_id( PayPalGateway::ID );
if ( $email && is_email( $email ) ) {
$payment_token_paypal->set_email( $email );
}
try {
$payment_token_paypal->save();
} catch ( Exception $exception ) {
$this->logger->error(
"Could not create WC payment token PayPal for customer {$customer_id}. " . $exception->getMessage()
);
}
return $payment_token_paypal->get_id();
}
/**
* Creates a WC Payment Token for Venmo payment.
*
* @param int $customer_id The WC customer ID.
* @param string $token The Venmo payment token.
* @param string $email The Venmo customer email.
*
* @return int
*/
public function create_payment_token_venmo(
int $customer_id,
string $token,
string $email
): int {
if ( $customer_id === 0 ) {
return 0;
}
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID );
if ( $this->payment_token_helper->token_exist( $wc_tokens, $token, PaymentTokenVenmo::class ) ) {
return 0;
}
// Try to update existing token of type before creating a new one.
$payment_token_venmo = $this->payment_token_helper->first_token_of_type( $wc_tokens, PaymentTokenVenmo::class );
if ( ! $payment_token_venmo ) {
$payment_token_venmo = $this->payment_token_factory->create( 'venmo' );
}
assert( $payment_token_venmo instanceof PaymentTokenVenmo );
$payment_token_venmo->set_token( $token );
$payment_token_venmo->set_user_id( $customer_id );
$payment_token_venmo->set_gateway_id( PayPalGateway::ID );
if ( $email && is_email( $email ) ) {
$payment_token_venmo->set_email( $email );
}
try {
$payment_token_venmo->save();
} catch ( Exception $exception ) {
$this->logger->error(
"Could not create WC payment token Venmo for customer {$customer_id}. " . $exception->getMessage()
);
}
return $payment_token_venmo->get_id();
}
/**
* Creates a WC Payment Token for ApplePay payment.
*
* @param int $customer_id The WC customer ID.
* @param string $token The ApplePay payment token.
*
* @return int
*/
public function create_payment_token_applepay(
int $customer_id,
string $token
): int {
if ( $customer_id === 0 ) {
return 0;
}
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID );
if ( $this->payment_token_helper->token_exist( $wc_tokens, $token, PaymentTokenApplePay::class ) ) {
return 0;
}
// Try to update existing token of type before creating a new one.
$payment_token_applepay = $this->payment_token_helper->first_token_of_type( $wc_tokens, PaymentTokenApplePay::class );
if ( ! $payment_token_applepay ) {
$payment_token_applepay = $this->payment_token_factory->create( 'apple_pay' );
}
assert( $payment_token_applepay instanceof PaymentTokenApplePay );
$payment_token_applepay->set_token( $token );
$payment_token_applepay->set_user_id( $customer_id );
$payment_token_applepay->set_gateway_id( PayPalGateway::ID );
try {
$payment_token_applepay->save();
} catch ( Exception $exception ) {
$this->logger->error(
"Could not create WC payment token ApplePay for customer {$customer_id}. " . $exception->getMessage()
);
}
return $payment_token_applepay->get_id();
}
/**
* Creates a WC Payment Token for Credit Card payment.
*
* @param int $customer_id The WC customer ID.
* @param stdClass $payment_token The Credit Card payment token.
*
* @return int
*/
public function create_payment_token_card( int $customer_id, stdClass $payment_token ): int {
if ( $customer_id === 0 ) {
return 0;
}
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, CreditCardGateway::ID );
if ( $this->payment_token_helper->token_exist( $wc_tokens, $payment_token->id ) ) {
return 0;
}
$token = new WC_Payment_Token_CC();
$token->set_token( $payment_token->id );
$token->set_user_id( $customer_id );
$token->set_gateway_id( CreditCardGateway::ID );
$token->set_last4( $payment_token->payment_source->card->last_digits ?? '' );
$expiry = explode( '-', $payment_token->payment_source->card->expiry ?? '' );
$token->set_expiry_year( $expiry[0] ?? '' );
$token->set_expiry_month( $expiry[1] ?? '' );
$brand = $payment_token->payment_source->card->brand ?? __( 'N/A', 'woocommerce-paypal-payments' );
if ( $brand ) {
$token->set_card_type( $brand );
}
try {
$token->save();
} catch ( Exception $exception ) {
$this->logger->error(
"Could not create WC payment token card for customer {$customer_id}. " . $exception->getMessage()
);
}
$token->save();
return $token->get_id();
}
/**
* Returns PayPal payment tokens for the given WP user id.
*
* @param int $user_id WP user id.
* @return array
*/
public function customer_tokens( int $user_id ): array {
$customer_id = get_user_meta( $user_id, '_ppcp_target_customer_id', true );
if ( ! $customer_id ) {
$customer_id = get_user_meta( $user_id, 'ppcp_customer_id', true );
}
try {
$customer_tokens = $this->payment_tokens_endpoint->payment_tokens_for_customer( $customer_id );
} catch ( RuntimeException $exception ) {
$customer_tokens = array();
}
return $customer_tokens;
}
/**
* Creates WC payment tokens for the given WP user id using PayPal payment tokens as source.
*
* @param array $customer_tokens PayPal customer payment tokens.
* @param int $user_id WP user id.
* @return void
*/
public function create_wc_tokens( array $customer_tokens, int $user_id ): void {
foreach ( $customer_tokens as $customer_token ) {
if ( $customer_token['payment_source']->name() === 'paypal' ) {
$this->create_payment_token_paypal(
$user_id,
$customer_token['id'],
$customer_token['payment_source']->properties()->email_address ?? ''
);
}
if ( $customer_token['payment_source']->name() === 'card' ) {
/**
* Suppress ArgumentTypeCoercion
*
* @psalm-suppress ArgumentTypeCoercion
*/
$this->create_payment_token_card(
$user_id,
(object) array(
'id' => $customer_token['id'],
'payment_source' => (object) array(
$customer_token['payment_source']->name() => $customer_token['payment_source']->properties(),
),
)
);
}
}
}
}

View file

@ -23,6 +23,8 @@ use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderReauthorizeAction;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
@ -102,7 +104,10 @@ return array(
$api_shop_country, $api_shop_country,
$container->get( 'api.endpoint.order' ), $container->get( 'api.endpoint.order' ),
$container->get( 'api.factory.paypal-checkout-url' ), $container->get( 'api.factory.paypal-checkout-url' ),
$container->get( 'wcgateway.place-order-button-text' ) $container->get( 'wcgateway.place-order-button-text' ),
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'vaulting.vault-v3-enabled' ),
$container->get( 'vaulting.wc-payment-tokens' )
); );
}, },
'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway { 'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway {
@ -128,9 +133,15 @@ return array(
$state, $state,
$transaction_url_provider, $transaction_url_provider,
$subscription_helper, $subscription_helper,
$logger,
$payments_endpoint, $payments_endpoint,
$vaulted_credit_card_handler $vaulted_credit_card_handler,
$container->get( 'onboarding.environment' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'wcgateway.endpoint.capture-card-payment' ),
$container->get( 'api.prefix' ),
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'vaulting.wc-payment-tokens' ),
$logger
); );
}, },
'wcgateway.card-button-gateway' => static function ( ContainerInterface $container ): CardButtonGateway { 'wcgateway.card-button-gateway' => static function ( ContainerInterface $container ): CardButtonGateway {
@ -381,19 +392,25 @@ return array(
$notice = $container->get( 'wcgateway.notice.authorize-order-action' ); $notice = $container->get( 'wcgateway.notice.authorize-order-action' );
$settings = $container->get( 'wcgateway.settings' ); $settings = $container->get( 'wcgateway.settings' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' ); $subscription_helper = $container->get( 'wc-subscriptions.helper' );
$amount_factory = $container->get( 'api.factory.amount' );
return new AuthorizedPaymentsProcessor( return new AuthorizedPaymentsProcessor(
$order_endpoint, $order_endpoint,
$payments_endpoint, $payments_endpoint,
$logger, $logger,
$notice, $notice,
$settings, $settings,
$subscription_helper $subscription_helper,
$amount_factory
); );
}, },
'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction { 'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction {
$column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); $column = $container->get( 'wcgateway.admin.orders-payment-status-column' );
return new RenderAuthorizeAction( $column ); return new RenderAuthorizeAction( $column );
}, },
'wcgateway.admin.render-reauthorize-action' => static function ( ContainerInterface $container ): RenderReauthorizeAction {
$column = $container->get( 'wcgateway.admin.orders-payment-status-column' );
return new RenderReauthorizeAction( $column );
},
'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail { 'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail {
$column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); $column = $container->get( 'wcgateway.admin.orders-payment-status-column' );
return new PaymentStatusOrderDetail( $column ); return new PaymentStatusOrderDetail( $column );
@ -405,7 +422,6 @@ return array(
'wcgateway.admin.fees-renderer' => static function ( ContainerInterface $container ): FeesRenderer { 'wcgateway.admin.fees-renderer' => static function ( ContainerInterface $container ): FeesRenderer {
return new FeesRenderer(); return new FeesRenderer();
}, },
'wcgateway.settings.should-render-settings' => static function ( ContainerInterface $container ): bool { 'wcgateway.settings.should-render-settings' => static function ( ContainerInterface $container ): bool {
$sections = array( $sections = array(
@ -420,13 +436,15 @@ return array(
return array_key_exists( $current_page_id, $sections ); return array_key_exists( $current_page_id, $sections );
}, },
'wcgateway.settings.fields.subscriptions_mode_options' => static function ( ContainerInterface $container ): array {
'wcgateway.settings.fields.subscriptions_mode' => static function ( ContainerInterface $container ): array { return array(
$subscription_mode_options = array(
'vaulting_api' => __( 'PayPal Vaulting', 'woocommerce-paypal-payments' ), 'vaulting_api' => __( 'PayPal Vaulting', 'woocommerce-paypal-payments' ),
'subscriptions_api' => __( 'PayPal Subscriptions', 'woocommerce-paypal-payments' ), 'subscriptions_api' => __( 'PayPal Subscriptions', 'woocommerce-paypal-payments' ),
'disable_paypal_subscriptions' => __( 'Disable PayPal for subscriptions', 'woocommerce-paypal-payments' ), 'disable_paypal_subscriptions' => __( 'Disable PayPal for subscriptions', 'woocommerce-paypal-payments' ),
); );
},
'wcgateway.settings.fields.subscriptions_mode' => static function ( ContainerInterface $container ): array {
$subscription_mode_options = $container->get( 'wcgateway.settings.fields.subscriptions_mode_options' );
$billing_agreements_endpoint = $container->get( 'api.endpoint.billing-agreements' ); $billing_agreements_endpoint = $container->get( 'api.endpoint.billing-agreements' );
$reference_transaction_enabled = $billing_agreements_endpoint->reference_transaction_enabled(); $reference_transaction_enabled = $billing_agreements_endpoint->reference_transaction_enabled();
@ -441,7 +459,7 @@ return array(
'input_class' => array( 'wc-enhanced-select' ), 'input_class' => array( 'wc-enhanced-select' ),
'desc_tip' => true, 'desc_tip' => true,
'description' => __( 'Utilize PayPal Vaulting for flexible subscription processing with saved payment methods, create “PayPal Subscriptions” to bill customers at regular intervals, or disable PayPal for subscription-type products.', 'woocommerce-paypal-payments' ), 'description' => __( 'Utilize PayPal Vaulting for flexible subscription processing with saved payment methods, create “PayPal Subscriptions” to bill customers at regular intervals, or disable PayPal for subscription-type products.', 'woocommerce-paypal-payments' ),
'default' => 'vaulting_api', 'default' => array_key_first( $subscription_mode_options ),
'options' => $subscription_mode_options, 'options' => $subscription_mode_options,
'screens' => array( 'screens' => array(
State::STATE_ONBOARDED, State::STATE_ONBOARDED,
@ -964,11 +982,6 @@ return array(
unset( $fields['subscriptions_mode'] ); unset( $fields['subscriptions_mode'] );
} }
$billing_agreements_endpoint = $container->get( 'api.endpoint.billing-agreements' );
if ( ! $billing_agreements_endpoint->reference_transaction_enabled() ) {
unset( $fields['vault_enabled'] );
}
/** /**
* Depending on your store location, some credit cards can't be used. * Depending on your store location, some credit cards can't be used.
* Here, we filter them out. * Here, we filter them out.
@ -1006,10 +1019,10 @@ return array(
'ideal' => _x( 'iDEAL', 'Name of payment method', 'woocommerce-paypal-payments' ), 'ideal' => _x( 'iDEAL', 'Name of payment method', 'woocommerce-paypal-payments' ),
'mybank' => _x( 'MyBank', 'Name of payment method', 'woocommerce-paypal-payments' ), 'mybank' => _x( 'MyBank', 'Name of payment method', 'woocommerce-paypal-payments' ),
'p24' => _x( 'Przelewy24', 'Name of payment method', 'woocommerce-paypal-payments' ), 'p24' => _x( 'Przelewy24', 'Name of payment method', 'woocommerce-paypal-payments' ),
'sofort' => _x( 'Sofort', 'Name of payment method', 'woocommerce-paypal-payments' ),
'venmo' => _x( 'Venmo', 'Name of payment method', 'woocommerce-paypal-payments' ), 'venmo' => _x( 'Venmo', 'Name of payment method', 'woocommerce-paypal-payments' ),
'trustly' => _x( 'Trustly', 'Name of payment method', 'woocommerce-paypal-payments' ), 'trustly' => _x( 'Trustly', 'Name of payment method', 'woocommerce-paypal-payments' ),
'paylater' => _x( 'Pay Later', 'Name of payment method', 'woocommerce-paypal-payments' ), 'paylater' => _x( 'PayPal Pay Later', 'Name of payment method', 'woocommerce-paypal-payments' ),
'paypal' => _x( 'PayPal', 'Name of payment method', 'woocommerce-paypal-payments' ),
); );
}, },
@ -1032,6 +1045,7 @@ return array(
array_flip( array_flip(
array( array(
'paylater', 'paylater',
'paypal',
) )
) )
); );
@ -1634,4 +1648,17 @@ return array(
) )
); );
}, },
'wcgateway.endpoint.capture-card-payment' => static function( ContainerInterface $container ): CaptureCardPayment {
return new CaptureCardPayment(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'api.factory.order' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'session.handler' ),
$container->get( 'wc-subscriptions.helpers.real-time-account-updater' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
); );

View file

@ -9,9 +9,6 @@ declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Admin; namespace WooCommerce\PayPalCommerce\WcGateway\Admin;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
/** /**
* Class RenderAuthorizeAction * Class RenderAuthorizeAction
*/ */

View file

@ -0,0 +1,67 @@
<?php
/**
* Renders the order action "Reauthorize PayPal payment"
*
* @package WooCommerce\PayPalCommerce\WcGateway\Admin
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Admin;
/**
* Class RenderReauthorizeAction
*/
class RenderReauthorizeAction {
/**
* The capture info column.
*
* @var OrderTablePaymentStatusColumn
*/
private $column;
/**
* PaymentStatusOrderDetail constructor.
*
* @param OrderTablePaymentStatusColumn $column The capture info column.
*/
public function __construct( OrderTablePaymentStatusColumn $column ) {
$this->column = $column;
}
/**
* Renders the action into the $order_actions array based on the WooCommerce order.
*
* @param array $order_actions The actions to render into.
* @param \WC_Order $wc_order The order for which to render the action.
*
* @return array
*/
public function render( array $order_actions, \WC_Order $wc_order ) : array {
if ( ! $this->should_render_for_order( $wc_order ) ) {
return $order_actions;
}
$order_actions['ppcp_reauthorize_order'] = esc_html__(
'Reauthorize PayPal payment',
'woocommerce-paypal-payments'
);
return $order_actions;
}
/**
* Whether the action should be rendered for a certain WooCommerce order.
*
* @param \WC_Order $order The Woocommerce order.
*
* @return bool
*/
private function should_render_for_order( \WC_Order $order ) : bool {
$status = $order->get_status();
$not_allowed_statuses = array( 'refunded', 'cancelled', 'failed' );
return $this->column->should_render_for_order( $order ) &&
! $this->column->is_captured( $order ) &&
! in_array( $status, $not_allowed_statuses, true );
}
}

View file

@ -9,12 +9,12 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Assets; namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\ResubscribeEndpoint;
/** /**
* Class SettingsPageAssets * Class SettingsPageAssets
@ -105,21 +105,29 @@ class SettingsPageAssets {
*/ */
private $is_acdc_enabled; private $is_acdc_enabled;
/**
* Billing Agreements endpoint.
*
* @var BillingAgreementsEndpoint
*/
private $billing_agreements_endpoint;
/** /**
* Assets constructor. * Assets constructor.
* *
* @param string $module_url The url of this module. * @param string $module_url The url of this module.
* @param string $version The assets version. * @param string $version The assets version.
* @param SubscriptionHelper $subscription_helper The subscription helper. * @param SubscriptionHelper $subscription_helper The subscription helper.
* @param string $client_id The PayPal SDK client ID. * @param string $client_id The PayPal SDK client ID.
* @param string $currency 3-letter currency code of the shop. * @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop. * @param string $country 2-letter country code of the shop.
* @param Environment $environment The environment object. * @param Environment $environment The environment object.
* @param bool $is_pay_later_button_enabled Whether Pay Later button is enabled either for checkout, cart or product page. * @param bool $is_pay_later_button_enabled Whether Pay Later button is enabled either for checkout, cart or product page.
* @param array $disabled_sources The list of disabled funding sources. * @param array $disabled_sources The list of disabled funding sources.
* @param array $all_funding_sources The list of all existing funding sources. * @param array $all_funding_sources The list of all existing funding sources.
* @param bool $is_settings_page Whether it's a settings page of this plugin. * @param bool $is_settings_page Whether it's a settings page of this plugin.
* @param bool $is_acdc_enabled Whether the ACDC gateway is enabled. * @param bool $is_acdc_enabled Whether the ACDC gateway is enabled.
* @param BillingAgreementsEndpoint $billing_agreements_endpoint Billing Agreements endpoint.
*/ */
public function __construct( public function __construct(
string $module_url, string $module_url,
@ -133,7 +141,8 @@ class SettingsPageAssets {
array $disabled_sources, array $disabled_sources,
array $all_funding_sources, array $all_funding_sources,
bool $is_settings_page, bool $is_settings_page,
bool $is_acdc_enabled bool $is_acdc_enabled,
BillingAgreementsEndpoint $billing_agreements_endpoint
) { ) {
$this->module_url = $module_url; $this->module_url = $module_url;
$this->version = $version; $this->version = $version;
@ -147,6 +156,7 @@ class SettingsPageAssets {
$this->all_funding_sources = $all_funding_sources; $this->all_funding_sources = $all_funding_sources;
$this->is_settings_page = $is_settings_page; $this->is_settings_page = $is_settings_page;
$this->is_acdc_enabled = $is_acdc_enabled; $this->is_acdc_enabled = $is_acdc_enabled;
$this->billing_agreements_endpoint = $billing_agreements_endpoint;
} }
/** /**
@ -250,6 +260,13 @@ class SettingsPageAssets {
), ),
), ),
), ),
'reference_transaction_enabled' => $this->billing_agreements_endpoint->reference_transaction_enabled(),
'vaulting_must_enable_advanced_wallet_message' => sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
esc_html__( 'Your PayPal account must be enabled for the %1$sAdvanced PayPal Wallet%2$s to use PayPal Vaulting.', 'woocommerce-paypal-payments' ),
'<a href="/wp-admin/admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&ppcp-tab=ppcp-connection#field-credentials_feature_onboarding_heading">',
'</a>'
),
) )
) )
); );

View file

@ -0,0 +1,186 @@
<?php
/**
* The Capture Card Payment endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Endpoint;
use Psr\Log\LoggerInterface;
use RuntimeException;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\RealTimeAccountUpdaterHelper;
use WP_Error;
/**
* Class CaptureCardPayment
*/
class CaptureCardPayment {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The order factory.
*
* @var OrderFactory
*/
private $order_factory;
/**
* The purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The session handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* Real Time Account Updater helper.
*
* @var RealTimeAccountUpdaterHelper
*/
private $real_time_account_updater_helper;
/**
* The settings.
*
* @var Settings
*/
private $settings;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* CaptureCardPayment constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param OrderFactory $order_factory The order factory.
* @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
* @param RealTimeAccountUpdaterHelper $real_time_account_updater_helper Real Time Account Updater helper.
* @param Settings $settings The settings.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
OrderFactory $order_factory,
PurchaseUnitFactory $purchase_unit_factory,
OrderEndpoint $order_endpoint,
SessionHandler $session_handler,
RealTimeAccountUpdaterHelper $real_time_account_updater_helper,
Settings $settings,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->order_factory = $order_factory;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->order_endpoint = $order_endpoint;
$this->session_handler = $session_handler;
$this->real_time_account_updater_helper = $real_time_account_updater_helper;
$this->settings = $settings;
$this->logger = $logger;
}
/**
* Creates PayPal order from the given card vault id.
*
* @param string $vault_id Vault id.
* @param string $custom_id Custom id.
* @param string $invoice_id Invoice id.
* @return stdClass
* @throws RuntimeException When request fails.
*/
public function create_order( string $vault_id, string $custom_id, string $invoice_id ): stdClass {
$intent = $this->settings->has( 'intent' ) && strtoupper( (string) $this->settings->get( 'intent' ) ) === 'AUTHORIZE' ? 'AUTHORIZE' : 'CAPTURE';
$items = array( $this->purchase_unit_factory->from_wc_cart() );
$data = array(
'intent' => $intent,
'purchase_units' => array_map(
static function ( PurchaseUnit $item ): array {
return $item->to_array( true, false );
},
$items
),
'payment_source' => array(
'card' => array(
'vault_id' => $vault_id,
'stored_credential' => array(
'payment_initiator' => 'CUSTOMER',
'payment_type' => 'UNSCHEDULED',
'usage' => 'SUBSEQUENT',
),
),
),
'custom_id' => $custom_id,
'invoice_id' => $invoice_id,
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
return json_decode( $response['body'] );
}
}

View file

@ -34,7 +34,7 @@ class FundingSourceRenderer {
* *
* @var string[] * @var string[]
*/ */
protected $own_funding_sources = array( 'venmo', 'paylater' ); protected $own_funding_sources = array( 'venmo', 'paylater', 'paypal' );
/** /**
* FundingSourceRenderer constructor. * FundingSourceRenderer constructor.
@ -56,12 +56,14 @@ class FundingSourceRenderer {
* @param string $id The ID of the funding source, such as 'venmo'. * @param string $id The ID of the funding source, such as 'venmo'.
*/ */
public function render_name( string $id ): string { public function render_name( string $id ): string {
$id = $this->sanitize_id( $id );
if ( array_key_exists( $id, $this->funding_sources ) ) { if ( array_key_exists( $id, $this->funding_sources ) ) {
if ( in_array( $id, $this->own_funding_sources, true ) ) { if ( in_array( $id, $this->own_funding_sources, true ) ) {
return $this->funding_sources[ $id ]; return $this->funding_sources[ $id ];
} }
return sprintf( return sprintf(
/* translators: %s - Sofort, BLIK, iDeal, Mercado Pago, etc. */ /* translators: %s - BLIK, iDeal, Mercado Pago, etc. */
__( '%s (via PayPal)', 'woocommerce-paypal-payments' ), __( '%s (via PayPal)', 'woocommerce-paypal-payments' ),
$this->funding_sources[ $id ] $this->funding_sources[ $id ]
); );
@ -78,9 +80,11 @@ class FundingSourceRenderer {
* @param string $id The ID of the funding source, such as 'venmo'. * @param string $id The ID of the funding source, such as 'venmo'.
*/ */
public function render_description( string $id ): string { public function render_description( string $id ): string {
$id = $this->sanitize_id( $id );
if ( array_key_exists( $id, $this->funding_sources ) ) { if ( array_key_exists( $id, $this->funding_sources ) ) {
return sprintf( return sprintf(
/* translators: %s - Sofort, BLIK, iDeal, Mercado Pago, etc. */ /* translators: %s - BLIK, iDeal, Mercado Pago, etc. */
__( 'Pay via %s.', 'woocommerce-paypal-payments' ), __( 'Pay via %s.', 'woocommerce-paypal-payments' ),
$this->funding_sources[ $id ] $this->funding_sources[ $id ]
); );
@ -90,4 +94,14 @@ class FundingSourceRenderer {
$this->settings->get( 'description' ) $this->settings->get( 'description' )
: __( 'Pay via PayPal.', 'woocommerce-paypal-payments' ); : __( 'Pay via PayPal.', 'woocommerce-paypal-payments' );
} }
/**
* Sanitizes the id to a standard format.
*
* @param string $id The funding source id.
* @return string
*/
private function sanitize_id( string $id ): string {
return str_replace( '_', '', strtolower( $id ) );
}
} }

View file

@ -13,27 +13,35 @@ use Exception;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WC_Order; use WC_Order;
use WC_Payment_Tokens; use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\Vaulting\VaultedCreditCardHandler; use WooCommerce\PayPalCommerce\Vaulting\VaultedCreditCardHandler;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
/** /**
* Class CreditCardGateway * Class CreditCardGateway
*/ */
class CreditCardGateway extends \WC_Payment_Gateway_CC { class CreditCardGateway extends \WC_Payment_Gateway_CC {
use ProcessPaymentTrait, GatewaySettingsRendererTrait, TransactionIdHandlingTrait; use ProcessPaymentTrait, GatewaySettingsRendererTrait, TransactionIdHandlingTrait, PaymentsStatusHandlingTrait, FreeTrialHandlerTrait;
const ID = 'ppcp-credit-card-gateway'; const ID = 'ppcp-credit-card-gateway';
@ -114,13 +122,6 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
*/ */
protected $subscription_helper; protected $subscription_helper;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/** /**
* The payments endpoint * The payments endpoint
* *
@ -128,6 +129,55 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
*/ */
protected $payments_endpoint; protected $payments_endpoint;
/**
* The environment.
*
* @var Environment
*/
private $environment;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* Capture card payment.
*
* @var CaptureCardPayment
*/
private $capture_card_payment;
/**
* The prefix.
*
* @var string
*/
private $prefix;
/**
* Payment tokens endpoint.
*
* @var PaymentTokensEndpoint
*/
private $payment_tokens_endpoint;
/**
* WooCommerce payment tokens factory.
*
* @var WooCommercePaymentTokens
*/
private $wc_payment_tokens;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/** /**
* CreditCardGateway constructor. * CreditCardGateway constructor.
* *
@ -140,9 +190,15 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
* @param State $state The state. * @param State $state The state.
* @param TransactionUrlProvider $transaction_url_provider Service able to provide view transaction url base. * @param TransactionUrlProvider $transaction_url_provider Service able to provide view transaction url base.
* @param SubscriptionHelper $subscription_helper The subscription helper. * @param SubscriptionHelper $subscription_helper The subscription helper.
* @param LoggerInterface $logger The logger.
* @param PaymentsEndpoint $payments_endpoint The payments endpoint. * @param PaymentsEndpoint $payments_endpoint The payments endpoint.
* @param VaultedCreditCardHandler $vaulted_credit_card_handler The vaulted credit card handler. * @param VaultedCreditCardHandler $vaulted_credit_card_handler The vaulted credit card handler.
* @param Environment $environment The environment.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param CaptureCardPayment $capture_card_payment Capture card payment.
* @param string $prefix The prefix.
* @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint.
* @param WooCommercePaymentTokens $wc_payment_tokens WooCommerce payment tokens factory.
* @param LoggerInterface $logger The logger.
*/ */
public function __construct( public function __construct(
SettingsRenderer $settings_renderer, SettingsRenderer $settings_renderer,
@ -154,9 +210,15 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
State $state, State $state,
TransactionUrlProvider $transaction_url_provider, TransactionUrlProvider $transaction_url_provider,
SubscriptionHelper $subscription_helper, SubscriptionHelper $subscription_helper,
LoggerInterface $logger,
PaymentsEndpoint $payments_endpoint, PaymentsEndpoint $payments_endpoint,
VaultedCreditCardHandler $vaulted_credit_card_handler VaultedCreditCardHandler $vaulted_credit_card_handler,
Environment $environment,
OrderEndpoint $order_endpoint,
CaptureCardPayment $capture_card_payment,
string $prefix,
PaymentTokensEndpoint $payment_tokens_endpoint,
WooCommercePaymentTokens $wc_payment_tokens,
LoggerInterface $logger
) { ) {
$this->id = self::ID; $this->id = self::ID;
$this->settings_renderer = $settings_renderer; $this->settings_renderer = $settings_renderer;
@ -168,9 +230,15 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
$this->state = $state; $this->state = $state;
$this->transaction_url_provider = $transaction_url_provider; $this->transaction_url_provider = $transaction_url_provider;
$this->subscription_helper = $subscription_helper; $this->subscription_helper = $subscription_helper;
$this->logger = $logger;
$this->payments_endpoint = $payments_endpoint; $this->payments_endpoint = $payments_endpoint;
$this->vaulted_credit_card_handler = $vaulted_credit_card_handler; $this->vaulted_credit_card_handler = $vaulted_credit_card_handler;
$this->environment = $environment;
$this->order_endpoint = $order_endpoint;
$this->capture_card_payment = $capture_card_payment;
$this->prefix = $prefix;
$this->payment_tokens_endpoint = $payment_tokens_endpoint;
$this->wc_payment_tokens = $wc_payment_tokens;
$this->logger = $logger;
if ( $state->current_state() === State::STATE_ONBOARDED ) { if ( $state->current_state() === State::STATE_ONBOARDED ) {
$this->supports = array( 'refunds' ); $this->supports = array( 'refunds' );
@ -246,8 +314,10 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
*/ */
public function form() { public function form() {
add_action( 'gettext', array( $this, 'replace_credit_card_cvv_label' ), 10, 3 ); add_action( 'gettext', array( $this, 'replace_credit_card_cvv_label' ), 10, 3 );
add_action( 'gettext', array( $this, 'replace_credit_card_cvv_placeholder' ), 10, 3 );
parent::form(); parent::form();
remove_action( 'gettext', 'replace_credit_card_cvv_label' ); remove_action( 'gettext', 'replace_credit_card_cvv_label' );
remove_action( 'gettext', 'replace_credit_card_cvv_placeholder' );
} }
/** /**
@ -267,6 +337,23 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
return __( 'CVV', 'woocommerce-paypal-payments' ); return __( 'CVV', 'woocommerce-paypal-payments' );
} }
/**
* Replace WooCommerce credit card CVV field placeholder.
*
* @param string $translation Translated text.
* @param string $text Original text to translate.
* @param string $domain Text domain.
*
* @return string Translated field.
*/
public function replace_credit_card_cvv_placeholder( string $translation, string $text, string $domain ): string {
if ( 'woocommerce' !== $domain || 'CVC' !== $text || ! apply_filters( 'woocommerce_paypal_payments_card_fields_translate_card_cvv', true ) ) {
return $translation;
}
return __( 'CVV', 'woocommerce-paypal-payments' );
}
/** /**
* Returns the icons of the gateway. * Returns the icons of the gateway.
* *
@ -366,17 +453,67 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
); );
} }
$saved_payment_card = WC()->session->get( 'ppcp_saved_payment_card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( $saved_payment_card ) { $card_payment_token_id = wc_clean( wp_unslash( $_POST['wc-ppcp-credit-card-gateway-payment-token'] ?? '' ) );
if ( $saved_payment_card['payment_source'] === 'card' && $saved_payment_card['status'] === 'COMPLETED' ) {
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $saved_payment_card['order_id'] );
$wc_order->save_meta_data();
$this->update_transaction_id( $saved_payment_card['order_id'], $wc_order ); if ( $this->is_free_trial_order( $wc_order ) && $card_payment_token_id ) {
$wc_order->payment_complete(); $customer_tokens = $this->wc_payment_tokens->customer_tokens( get_current_user_id() );
WC()->session->set( 'ppcp_saved_payment_card', null ); foreach ( $customer_tokens as $token ) {
if ( $token['payment_source']->name() === 'card' ) {
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
}
}
return $this->handle_payment_success( $wc_order ); if ( $card_payment_token_id ) {
$customer_tokens = $this->wc_payment_tokens->customer_tokens( get_current_user_id() );
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id(), self::ID );
if ( $customer_tokens && empty( $wc_tokens ) ) {
$this->wc_payment_tokens->create_wc_tokens( $customer_tokens, get_current_user_id() );
}
$customer_token_ids = array();
foreach ( $customer_tokens as $customer_token ) {
$customer_token_ids[] = $customer_token['id'];
}
$tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id() );
foreach ( $tokens as $token ) {
if ( $token->get_id() === (int) $card_payment_token_id ) {
if ( ! in_array( $token->get_token(), $customer_token_ids, true ) ) {
$token->delete();
continue;
}
$custom_id = $wc_order->get_order_number();
$invoice_id = $this->prefix . $wc_order->get_order_number();
$create_order = $this->capture_card_payment->create_order( $token->get_token(), $custom_id, $invoice_id );
$order = $this->order_endpoint->order( $create_order->id );
$wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() );
if ( $order->intent() === 'AUTHORIZE' ) {
$order = $this->order_endpoint->authorize( $order );
$wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'false' );
if ( $this->subscription_helper->has_subscription( $wc_order->get_id() ) ) {
$wc_order->update_meta_data( '_ppcp_captured_vault_webhook', 'false' );
}
}
$transaction_id = $this->get_paypal_order_transaction_id( $order );
if ( $transaction_id ) {
$this->update_transaction_id( $transaction_id, $wc_order );
}
$this->handle_new_order_status( $order, $wc_order );
return $this->handle_payment_success( $wc_order );
}
} }
} }

View file

@ -14,12 +14,14 @@ use Psr\Log\LoggerInterface;
use WC_Order; use WC_Order;
use WC_Payment_Tokens; use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
@ -48,9 +50,17 @@ class PayPalGateway extends \WC_Payment_Gateway {
const ORDER_ID_META_KEY = '_ppcp_paypal_order_id'; const ORDER_ID_META_KEY = '_ppcp_paypal_order_id';
const ORDER_PAYMENT_MODE_META_KEY = '_ppcp_paypal_payment_mode'; const ORDER_PAYMENT_MODE_META_KEY = '_ppcp_paypal_payment_mode';
const ORDER_PAYMENT_SOURCE_META_KEY = '_ppcp_paypal_payment_source'; const ORDER_PAYMENT_SOURCE_META_KEY = '_ppcp_paypal_payment_source';
const ORDER_PAYER_EMAIL_META_KEY = '_ppcp_paypal_payer_email';
const FEES_META_KEY = '_ppcp_paypal_fees'; const FEES_META_KEY = '_ppcp_paypal_fees';
const REFUND_FEES_META_KEY = '_ppcp_paypal_refund_fees'; const REFUND_FEES_META_KEY = '_ppcp_paypal_refund_fees';
const REFUNDS_META_KEY = '_ppcp_refunds'; const REFUNDS_META_KEY = '_ppcp_refunds';
const THREE_D_AUTH_RESULT_META_KEY = '_ppcp_paypal_3DS_auth_result';
const FRAUD_RESULT_META_KEY = '_ppcp_paypal_fraud_result';
/**
* List of payment sources wich we are expected to store the payer email in the WC Order metadata.
*/
const PAYMENT_SOURCES_WITH_PAYER_EMAIL = array( 'paypal', 'paylater', 'venmo' );
/** /**
* The Settings Renderer. * The Settings Renderer.
@ -171,26 +181,50 @@ class PayPalGateway extends \WC_Payment_Gateway {
*/ */
private $paypal_checkout_url_factory; private $paypal_checkout_url_factory;
/**
* Payment tokens endpoint.
*
* @var PaymentTokensEndpoint
*/
private $payment_tokens_endpoint;
/**
* Whether Vault v3 module is enabled.
*
* @var bool
*/
private $vault_v3_enabled;
/**
* WooCommerce payment tokens.
*
* @var WooCommercePaymentTokens
*/
private $wc_payment_tokens;
/** /**
* PayPalGateway constructor. * PayPalGateway constructor.
* *
* @param SettingsRenderer $settings_renderer The Settings Renderer. * @param SettingsRenderer $settings_renderer The Settings Renderer.
* @param FundingSourceRenderer $funding_source_renderer The funding source renderer. * @param FundingSourceRenderer $funding_source_renderer The funding source renderer.
* @param OrderProcessor $order_processor The Order Processor. * @param OrderProcessor $order_processor The Order Processor.
* @param ContainerInterface $config The settings. * @param ContainerInterface $config The settings.
* @param SessionHandler $session_handler The Session Handler. * @param SessionHandler $session_handler The Session Handler.
* @param RefundProcessor $refund_processor The Refund Processor. * @param RefundProcessor $refund_processor The Refund Processor.
* @param State $state The state. * @param State $state The state.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order. * @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
* @param SubscriptionHelper $subscription_helper The subscription helper. * @param SubscriptionHelper $subscription_helper The subscription helper.
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
* @param Environment $environment The environment. * @param Environment $environment The environment.
* @param PaymentTokenRepository $payment_token_repository The payment token repository. * @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
* @param string $api_shop_country The api shop country. * @param string $api_shop_country The api shop country.
* @param OrderEndpoint $order_endpoint The order endpoint. * @param OrderEndpoint $order_endpoint The order endpoint.
* @param callable(string):string $paypal_checkout_url_factory The function return the PayPal checkout URL for the given order ID. * @param callable(string):string $paypal_checkout_url_factory The function return the PayPal checkout URL for the given order ID.
* @param string $place_order_button_text The text for the standard "Place order" button. * @param string $place_order_button_text The text for the standard "Place order" button.
* @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint.
* @param bool $vault_v3_enabled Whether Vault v3 module is enabled.
* @param WooCommercePaymentTokens $wc_payment_tokens WooCommerce payment tokens.
*/ */
public function __construct( public function __construct(
SettingsRenderer $settings_renderer, SettingsRenderer $settings_renderer,
@ -209,7 +243,10 @@ class PayPalGateway extends \WC_Payment_Gateway {
string $api_shop_country, string $api_shop_country,
OrderEndpoint $order_endpoint, OrderEndpoint $order_endpoint,
callable $paypal_checkout_url_factory, callable $paypal_checkout_url_factory,
string $place_order_button_text string $place_order_button_text,
PaymentTokensEndpoint $payment_tokens_endpoint,
bool $vault_v3_enabled,
WooCommercePaymentTokens $wc_payment_tokens
) { ) {
$this->id = self::ID; $this->id = self::ID;
$this->settings_renderer = $settings_renderer; $this->settings_renderer = $settings_renderer;
@ -229,6 +266,10 @@ class PayPalGateway extends \WC_Payment_Gateway {
$this->api_shop_country = $api_shop_country; $this->api_shop_country = $api_shop_country;
$this->paypal_checkout_url_factory = $paypal_checkout_url_factory; $this->paypal_checkout_url_factory = $paypal_checkout_url_factory;
$this->order_button_text = $place_order_button_text; $this->order_button_text = $place_order_button_text;
$this->order_endpoint = $order_endpoint;
$this->payment_tokens_endpoint = $payment_tokens_endpoint;
$this->vault_v3_enabled = $vault_v3_enabled;
$this->wc_payment_tokens = $wc_payment_tokens;
if ( $this->onboarded ) { if ( $this->onboarded ) {
$this->supports = array( 'refunds', 'tokenization' ); $this->supports = array( 'refunds', 'tokenization' );
@ -291,8 +332,6 @@ class PayPalGateway extends \WC_Payment_Gateway {
'process_admin_options', 'process_admin_options',
) )
); );
$this->order_endpoint = $order_endpoint;
} }
/** /**
@ -394,13 +433,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
} }
if ( $this->is_pay_later_tab() ) { if ( $this->is_pay_later_tab() ) {
return sprintf( return '';
// translators: %1$s is </ br> HTML tag and %2$s, %3$s are the opening and closing of HTML <i> tag.
__( 'Let customers pay over time while you get paid up front — at no additional cost.%1$sPayPals pay later options are boosting merchant conversion rates and increasing cart sizes by 39%%. %2$s(PayPal Q2 Earnings-2021.)%3$s', 'woocommerce-paypal-payments' ),
'</ br>',
'<i>',
'</ i>'
);
} }
if ( is_admin() ) { if ( is_admin() ) {
@ -498,7 +531,49 @@ class PayPalGateway extends \WC_Payment_Gateway {
$wc_order->save(); $wc_order->save();
} }
if ( 'card' !== $funding_source && $this->is_free_trial_order( $wc_order ) && ! $this->subscription_helper->paypal_subscription_id() ) { if (
'card' !== $funding_source
&& $this->is_free_trial_order( $wc_order )
&& ! $this->subscription_helper->paypal_subscription_id()
) {
$ppcp_guest_payment_for_free_trial = WC()->session->get( 'ppcp_guest_payment_for_free_trial' ) ?? null;
if ( $this->vault_v3_enabled && $ppcp_guest_payment_for_free_trial ) {
$customer_id = $ppcp_guest_payment_for_free_trial->customer->id ?? '';
if ( $customer_id ) {
update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id );
}
if ( isset( $ppcp_guest_payment_for_free_trial->payment_source->paypal ) ) {
$email = '';
if ( isset( $ppcp_guest_payment_for_free_trial->payment_source->paypal->email_address ) ) {
$email = $ppcp_guest_payment_for_free_trial->payment_source->paypal->email_address;
}
$this->wc_payment_tokens->create_payment_token_paypal(
$wc_order->get_customer_id(),
$ppcp_guest_payment_for_free_trial->id,
$email
);
}
WC()->session->set( 'ppcp_guest_payment_for_free_trial', null );
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
$customer_id = get_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', true );
if ( $customer_id ) {
$customer_tokens = $this->payment_tokens_endpoint->payment_tokens_for_customer( $customer_id );
foreach ( $customer_tokens as $token ) {
$payment_source_name = $token['payment_source']->name() ?? '';
if ( $payment_source_name === 'paypal' || $payment_source_name === 'venmo' ) {
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
}
}
$user_id = (int) $wc_order->get_customer_id(); $user_id = (int) $wc_order->get_customer_id();
$tokens = $this->payment_token_repository->all_for_user_id( $user_id ); $tokens = $this->payment_token_repository->all_for_user_id( $user_id );
if ( ! array_filter( if ( ! array_filter(
@ -511,7 +586,6 @@ class PayPalGateway extends \WC_Payment_Gateway {
} }
$wc_order->payment_complete(); $wc_order->payment_complete();
return $this->handle_payment_success( $wc_order ); return $this->handle_payment_success( $wc_order );
} }

View file

@ -198,25 +198,41 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
'description' => __( "Specify brand name, logo and customer service instructions to be presented on Ratepay's payment instructions.", 'woocommerce-paypal-payments' ), 'description' => __( "Specify brand name, logo and customer service instructions to be presented on Ratepay's payment instructions.", 'woocommerce-paypal-payments' ),
), ),
'brand_name' => array( 'brand_name' => array(
'title' => __( 'Brand name', 'woocommerce-paypal-payments' ), 'title' => __( 'Brand name', 'woocommerce-paypal-payments' ),
'type' => 'text', 'type' => 'text',
'default' => get_bloginfo( 'name' ) ?? '', 'default' => get_bloginfo( 'name' ) ?? '',
'desc_tip' => true, 'desc_tip' => true,
'description' => __( 'Merchant name displayed in Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ), 'description' => __( 'Merchant name displayed in Ratepay\'s payment instructions. Should not exceed 127 characters.', 'woocommerce-paypal-payments' ),
'maxlength' => 127,
'custom_attributes' => array(
'pattern' => '.{1,127}',
'autocomplete' => 'off',
'required' => '',
),
), ),
'logo_url' => array( 'logo_url' => array(
'title' => __( 'Logo URL', 'woocommerce-paypal-payments' ), 'title' => __( 'Logo URL', 'woocommerce-paypal-payments' ),
'type' => 'url', 'type' => 'url',
'default' => '', 'default' => '',
'desc_tip' => true, 'desc_tip' => true,
'description' => __( 'Logo to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ), 'description' => __( 'Logo to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ),
'custom_attributes' => array(
'pattern' => '.+',
'autocomplete' => 'off',
'required' => '',
),
), ),
'customer_service_instructions' => array( 'customer_service_instructions' => array(
'title' => __( 'Customer service instructions', 'woocommerce-paypal-payments' ), 'title' => __( 'Customer service instructions', 'woocommerce-paypal-payments' ),
'type' => 'text', 'type' => 'text',
'default' => '', 'default' => '',
'desc_tip' => true, 'desc_tip' => true,
'description' => __( 'Customer service instructions to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ), 'description' => __( 'Customer service instructions to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ),
'custom_attributes' => array(
'pattern' => '.+',
'autocomplete' => 'off',
'required' => '',
),
), ),
); );
} }

View file

@ -10,6 +10,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Processor; namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use Exception; use Exception;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WC_Order; use WC_Order;
@ -91,6 +93,20 @@ class AuthorizedPaymentsProcessor {
*/ */
private $subscription_helper; private $subscription_helper;
/**
* The amount factory.
*
* @var AmountFactory
*/
private $amount_factory;
/**
* The reauthorization failure reason.
*
* @var string
*/
private $reauthorization_failure_reason = '';
/** /**
* AuthorizedPaymentsProcessor constructor. * AuthorizedPaymentsProcessor constructor.
* *
@ -100,6 +116,7 @@ class AuthorizedPaymentsProcessor {
* @param AuthorizeOrderActionNotice $notice The notice. * @param AuthorizeOrderActionNotice $notice The notice.
* @param ContainerInterface $config The settings. * @param ContainerInterface $config The settings.
* @param SubscriptionHelper $subscription_helper The subscription helper. * @param SubscriptionHelper $subscription_helper The subscription helper.
* @param AmountFactory $amount_factory The amount factory.
*/ */
public function __construct( public function __construct(
OrderEndpoint $order_endpoint, OrderEndpoint $order_endpoint,
@ -107,7 +124,8 @@ class AuthorizedPaymentsProcessor {
LoggerInterface $logger, LoggerInterface $logger,
AuthorizeOrderActionNotice $notice, AuthorizeOrderActionNotice $notice,
ContainerInterface $config, ContainerInterface $config,
SubscriptionHelper $subscription_helper SubscriptionHelper $subscription_helper,
AmountFactory $amount_factory
) { ) {
$this->order_endpoint = $order_endpoint; $this->order_endpoint = $order_endpoint;
@ -116,6 +134,7 @@ class AuthorizedPaymentsProcessor {
$this->notice = $notice; $this->notice = $notice;
$this->config = $config; $this->config = $config;
$this->subscription_helper = $subscription_helper; $this->subscription_helper = $subscription_helper;
$this->amount_factory = $amount_factory;
} }
/** /**
@ -249,6 +268,67 @@ class AuthorizedPaymentsProcessor {
} }
} }
/**
* Reauthorizes an authorized payment for an WooCommerce order.
*
* @param WC_Order $wc_order The WooCommerce order.
*
* @return string The status or reauthorization id.
*/
public function reauthorize_payment( WC_Order $wc_order ): string {
$this->reauthorization_failure_reason = '';
try {
$order = $this->paypal_order_from_wc_order( $wc_order );
} catch ( Exception $exception ) {
$this->logger->error( 'Could not get PayPal order from WC order: ' . $exception->getMessage() );
if ( $exception->getCode() === 404 ) {
return self::NOT_FOUND;
}
return self::INACCESSIBLE;
}
$amount = $this->amount_factory->from_wc_order( $wc_order );
$authorizations = $this->all_authorizations( $order );
$uncaptured_authorizations = $this->authorizations_to_capture( ...$authorizations );
if ( ! $uncaptured_authorizations ) {
if ( $this->captured_authorizations( ...$authorizations ) ) {
$this->logger->info( 'Authorizations already captured.' );
return self::ALREADY_CAPTURED;
}
$this->logger->info( 'Bad authorization.' );
return self::BAD_AUTHORIZATION;
}
$authorization = end( $uncaptured_authorizations );
try {
$this->payments_endpoint->reauthorize( $authorization->id(), new Money( $amount->value(), $amount->currency_code() ) );
} catch ( PayPalApiException $exception ) {
$this->reauthorization_failure_reason = $exception->details()[0]->description ?? null;
$this->logger->error( 'Reauthorization failed: ' . $exception->name() . ' | ' . $this->reauthorization_failure_reason );
return self::FAILED;
} catch ( Exception $exception ) {
$this->logger->error( 'Failed to capture authorization: ' . $exception->getMessage() );
return self::FAILED;
}
return self::SUCCESSFUL;
}
/**
* The reason for a failed reauthorization.
*
* @return string
*/
public function reauthorization_failure_reason(): string {
return $this->reauthorization_failure_reason;
}
/** /**
* Voids authorizations for the given PayPal order. * Voids authorizations for the given PayPal order.
* *
@ -392,4 +472,5 @@ class AuthorizedPaymentsProcessor {
} }
); );
} }
} }

View file

@ -0,0 +1,158 @@
<?php
/**
* Common operations performed for handling the ACDC order info.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Processor
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\FraudProcessorResponse;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Trait CreditCardOrderInfoHandlingTrait.
*/
trait CreditCardOrderInfoHandlingTrait {
/**
* Handles the 3DS details.
*
* Adds the order note with 3DS details.
* Adds the order meta with 3DS details.
*
* @param Order $order The PayPal order.
* @param WC_Order $wc_order The WC order.
*/
protected function handle_three_d_secure(
Order $order,
WC_Order $wc_order
): void {
$payment_source = $order->payment_source();
if ( ! $payment_source || $payment_source->name() !== 'card' ) {
return;
}
$authentication_result = $payment_source->properties()->authentication_result ?? null;
if ( $authentication_result ) {
$card_authentication_result_factory = new CardAuthenticationResultFactory();
$result = $card_authentication_result_factory->from_paypal_response( $authentication_result );
$three_d_response_order_note_title = __( '3DS Authentication Result', 'woocommerce-paypal-payments' );
/* translators: %1$s is 3DS order note title, %2$s is 3DS order note result markup */
$three_d_response_order_note_format = __( '%1$s %2$s', 'woocommerce-paypal-payments' );
$three_d_response_order_note_result_format = '<ul class="ppcp_3ds_result">
<li>%1$s</li>
<li>%2$s</li>
<li>%3$s</li>
</ul>';
$three_d_response_order_note_result = sprintf(
$three_d_response_order_note_result_format,
/* translators: %s is liability shift */
sprintf( __( 'Liability Shift: %s', 'woocommerce-paypal-payments' ), esc_html( $result->liability_shift() ) ),
/* translators: %s is enrollment status */
sprintf( __( 'Enrollment Status: %s', 'woocommerce-paypal-payments' ), esc_html( $result->enrollment_status() ) ),
/* translators: %s is authentication status */
sprintf( __( 'Authentication Status: %s', 'woocommerce-paypal-payments' ), esc_html( $result->authentication_result() ) )
);
$three_d_response_order_note = sprintf(
$three_d_response_order_note_format,
esc_html( $three_d_response_order_note_title ),
wp_kses_post( $three_d_response_order_note_result )
);
$wc_order->add_order_note( $three_d_response_order_note );
$wc_order->update_meta_data( PayPalGateway::THREE_D_AUTH_RESULT_META_KEY, $result->to_array() );
$wc_order->save_meta_data();
/**
* Fired when the 3DS information is added to WC order.
*/
do_action( 'woocommerce_paypal_payments_three_d_secure_added', $wc_order, $order );
}
}
/**
* Handles the fraud processor response details.
*
* Adds the order note with the fraud processor response details.
* Adds the order meta with the fraud processor response details.
*
* @param FraudProcessorResponse $fraud The fraud processor response (AVS, CVV ...).
* @param Order $order The PayPal order.
* @param WC_Order $wc_order The WC order.
*/
protected function handle_fraud( FraudProcessorResponse $fraud, Order $order, WC_Order $wc_order ): void {
$payment_source = $order->payment_source();
if ( ! $payment_source || $payment_source->name() !== 'card' ) {
return;
}
$fraud_responses = $fraud->to_array();
$card_brand = $payment_source->properties()->brand ?? __( 'N/A', 'woocommerce-paypal-payments' );
$card_last_digits = $payment_source->properties()->last_digits ?? __( 'N/A', 'woocommerce-paypal-payments' );
$avs_response_order_note_title = __( 'Address Verification Result', 'woocommerce-paypal-payments' );
/* translators: %1$s is AVS order note title, %2$s is AVS order note result markup */
$avs_response_order_note_format = __( '%1$s %2$s', 'woocommerce-paypal-payments' );
$avs_response_order_note_result_format = '<ul class="ppcp_avs_result">
<li>%1$s</li>
<ul class="ppcp_avs_result_inner">
<li>%2$s</li>
<li>%3$s</li>
</ul>
<li>%4$s</li>
<li>%5$s</li>
</ul>';
$avs_response_order_note_result = sprintf(
$avs_response_order_note_result_format,
/* translators: %s is fraud AVS code */
sprintf( __( 'AVS: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['avs_code'] ) ),
/* translators: %s is fraud AVS address match */
sprintf( __( 'Address Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['address_match'] ) ),
/* translators: %s is fraud AVS postal match */
sprintf( __( 'Postal Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['postal_match'] ) ),
/* translators: %s is card brand */
sprintf( __( 'Card Brand: %s', 'woocommerce-paypal-payments' ), esc_html( $card_brand ) ),
/* translators: %s card last digits */
sprintf( __( 'Card Last Digits: %s', 'woocommerce-paypal-payments' ), esc_html( $card_last_digits ) )
);
$avs_response_order_note = sprintf(
$avs_response_order_note_format,
esc_html( $avs_response_order_note_title ),
wp_kses_post( $avs_response_order_note_result )
);
$wc_order->add_order_note( $avs_response_order_note );
$cvv_response_order_note_format = '<ul class="ppcp_cvv_result"><li>%1$s</li></ul>';
$cvv_response_order_note = sprintf(
$cvv_response_order_note_format,
/* translators: %s is fraud CVV match */
sprintf( __( 'CVV2 Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['cvv_match'] ) )
);
$wc_order->add_order_note( $cvv_response_order_note );
$meta_details = array_merge(
$fraud_responses,
array(
'card_brand' => $card_brand,
'card_last_digits' => $card_last_digits,
)
);
$wc_order->update_meta_data( PayPalGateway::FRAUD_RESULT_META_KEY, $meta_details );
$wc_order->save_meta_data();
/**
* Fired when the fraud result information is added to WC order.
*/
do_action( 'woocommerce_paypal_payments_fraud_result_added', $wc_order, $order );
}
}

View file

@ -45,6 +45,18 @@ trait OrderMetaTrait {
$wc_order->update_meta_data( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY, $payment_source ); $wc_order->update_meta_data( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY, $payment_source );
} }
$payer = $order->payer();
if (
$payer
&& $payment_source
&& in_array( $payment_source, PayPalGateway::PAYMENT_SOURCES_WITH_PAYER_EMAIL, true )
) {
$payer_email = $payer->email_address();
if ( $payer_email ) {
$wc_order->update_meta_data( PayPalGateway::ORDER_PAYER_EMAIL_META_KEY, $payer_email );
}
}
$wc_order->save(); $wc_order->save();
do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order ); do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order );

View file

@ -112,6 +112,10 @@ trait PaymentsStatusHandlingTrait {
'on-hold', 'on-hold',
__( 'Awaiting payment.', 'woocommerce-paypal-payments' ) __( 'Awaiting payment.', 'woocommerce-paypal-payments' )
); );
/**
* Fired when PayPal order is authorized.
*/
do_action( 'woocommerce_paypal_payments_order_authorized', $wc_order, $authorization );
break; break;
case AuthorizationStatus::DENIED: case AuthorizationStatus::DENIED:
$wc_order->update_status( $wc_order->update_status(

View file

@ -22,6 +22,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundCapture; use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundCapture;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater; use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
@ -107,6 +109,10 @@ class RefundProcessor {
*/ */
public function process( WC_Order $wc_order, float $amount = null, string $reason = '' ) : bool { public function process( WC_Order $wc_order, float $amount = null, string $reason = '' ) : bool {
try { try {
if ( ! in_array( $wc_order->get_payment_method(), array( PayPalGateway::ID, CreditCardGateway::ID, CardButtonGateway::ID ), true ) ) {
return true;
}
$order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY ); $order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $order_id ) { if ( ! $order_id ) {
throw new RuntimeException( 'PayPal order ID not found in meta.' ); throw new RuntimeException( 'PayPal order ID not found in meta.' );

View file

@ -65,7 +65,7 @@ return function ( ContainerInterface $container, array $fields ): array {
<a href="https://woo.com/document/woocommerce-paypal-payments/#paypal-card-processing-acdc" target="_blank"><img alt="American Express" src="' . esc_url( $module_url ) . 'assets/images/amex.svg"/></a> <a href="https://woo.com/document/woocommerce-paypal-payments/#paypal-card-processing-acdc" target="_blank"><img alt="American Express" src="' . esc_url( $module_url ) . 'assets/images/amex.svg"/></a>
<a href="https://woo.com/document/woocommerce-paypal-payments/#paypal-card-processing-acdc" target="_blank"><img alt="Discover" src="' . esc_url( $module_url ) . 'assets/images/discover.svg"/></a> <a href="https://woo.com/document/woocommerce-paypal-payments/#paypal-card-processing-acdc" target="_blank"><img alt="Discover" src="' . esc_url( $module_url ) . 'assets/images/discover.svg"/></a>
<a href="https://woo.com/document/woocommerce-paypal-payments/#alternative-payment-methods" target="_blank"><img alt="iDEAL" src="' . esc_url( $module_url ) . 'assets/images/ideal-dark.svg"/></a> <a href="https://woo.com/document/woocommerce-paypal-payments/#alternative-payment-methods" target="_blank"><img alt="iDEAL" src="' . esc_url( $module_url ) . 'assets/images/ideal-dark.svg"/></a>
<a href="https://woo.com/document/woocommerce-paypal-payments/#alternative-payment-methods" target="_blank"><img alt="Sofort" src="' . esc_url( $module_url ) . 'assets/images/sofort.svg"/></a> <a href="https://woo.com/document/woocommerce-paypal-payments/#alternative-payment-methods" target="_blank"><img alt="BLIK" src="' . esc_url( $module_url ) . 'assets/images/blik.svg"/></a>
</div> </div>
<div class="ppcp-onboarding-header-apm-logos"> <div class="ppcp-onboarding-header-apm-logos">
<a href="https://woo.com/document/woocommerce-paypal-payments/#apple-pay" target="_blank"><img alt="Apple Pay" src="' . esc_url( $module_url ) . 'assets/images/button-Apple-Pay.png"/></a> <a href="https://woo.com/document/woocommerce-paypal-payments/#apple-pay" target="_blank"><img alt="Apple Pay" src="' . esc_url( $module_url ) . 'assets/images/button-Apple-Pay.png"/></a>

Some files were not shown because too many files have changed in this diff Show more