1%
diff --git a/composer.json b/composer.json
index f5b03aaa04f..13f17e9c000 100644
--- a/composer.json
+++ b/composer.json
@@ -22,10 +22,10 @@
"require": {
"php": ">=7.3",
"ext-json": "*",
- "automattic/jetpack-connection": "2.12.4",
- "automattic/jetpack-config": "2.0.4",
- "automattic/jetpack-autoloader": "3.0.10",
- "automattic/jetpack-sync": "3.8.0",
+ "automattic/jetpack-connection": "6.2.0",
+ "automattic/jetpack-config": "3.0.0",
+ "automattic/jetpack-autoloader": "5.0.0",
+ "automattic/jetpack-sync": "4.1.0",
"woocommerce/subscriptions-core": "6.7.1"
},
"require-dev": {
diff --git a/composer.lock b/composer.lock
index f6276dc29e7..3e1d4ee08df 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,27 +4,27 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "2f2c365c1ebb8b6af6e0df8c0ba64709",
+ "content-hash": "ed20d78f8b2b14b67df2266bd7614d62",
"packages": [
{
"name": "automattic/jetpack-a8c-mc-stats",
- "version": "v2.0.4",
+ "version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-a8c-mc-stats.git",
- "reference": "d1d726e4962d4bf6f9c51d01e63d613c3a9dd0bb"
+ "reference": "d6bdf2f1d1941e0a22d17c6f3152097d8e0a30e6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/d1d726e4962d4bf6f9c51d01e63d613c3a9dd0bb",
- "reference": "d1d726e4962d4bf6f9c51d01e63d613c3a9dd0bb",
+ "url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/d6bdf2f1d1941e0a22d17c6f3152097d8e0a30e6",
+ "reference": "d6bdf2f1d1941e0a22d17c6f3152097d8e0a30e6",
"shasum": ""
},
"require": {
- "php": ">=7.0"
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.8",
+ "automattic/jetpack-changelogger": "^5.0.0",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -34,11 +34,11 @@
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-a8c-mc-stats",
+ "branch-alias": {
+ "dev-trunk": "3.0.x-dev"
+ },
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v${old}...v${new}"
- },
- "branch-alias": {
- "dev-trunk": "2.0.x-dev"
}
},
"autoload": {
@@ -52,31 +52,31 @@
],
"description": "Used to record internal usage stats for Automattic. Not visible to site owners.",
"support": {
- "source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v2.0.4"
+ "source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v3.0.0"
},
- "time": "2024-11-04T09:23:35+00:00"
+ "time": "2024-11-14T20:12:50+00:00"
},
{
"name": "automattic/jetpack-admin-ui",
- "version": "v0.4.6",
+ "version": "v0.5.1",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-admin-ui.git",
- "reference": "a7bef1b075e35e431c0112f97763df9c6196ae39"
+ "reference": "a0894d34333451089add7b20f70e73b6509d6b6d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/a7bef1b075e35e431c0112f97763df9c6196ae39",
- "reference": "a7bef1b075e35e431c0112f97763df9c6196ae39",
+ "url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/a0894d34333451089add7b20f70e73b6509d6b6d",
+ "reference": "a0894d34333451089add7b20f70e73b6509d6b6d",
"shasum": ""
},
"require": {
- "php": ">=7.0"
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.8",
- "automattic/jetpack-logo": "^2.0.5",
- "automattic/wordbless": "dev-master",
+ "automattic/jetpack-changelogger": "^5.1.0",
+ "automattic/jetpack-logo": "^3.0.0",
+ "automattic/wordbless": "^0.4.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -85,14 +85,14 @@
"type": "jetpack-library",
"extra": {
"autotagger": true,
- "mirror-repo": "Automattic/jetpack-admin-ui",
"textdomain": "jetpack-admin-ui",
+ "mirror-repo": "Automattic/jetpack-admin-ui",
+ "branch-alias": {
+ "dev-trunk": "0.5.x-dev"
+ },
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-admin-ui/compare/${old}...${new}"
},
- "branch-alias": {
- "dev-trunk": "0.4.x-dev"
- },
"version-constants": {
"::PACKAGE_VERSION": "src/class-admin-menu.php"
}
@@ -108,31 +108,31 @@
],
"description": "Generic Jetpack wp-admin UI elements",
"support": {
- "source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.4.6"
+ "source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.5.1"
},
- "time": "2024-11-04T09:23:52+00:00"
+ "time": "2024-11-25T16:33:45+00:00"
},
{
"name": "automattic/jetpack-assets",
- "version": "v2.3.13",
+ "version": "v4.0.1",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-assets.git",
- "reference": "c520bffce576c823d7cbc851198201a820b7f981"
+ "reference": "ca1ebeceeeafb31876a234fa68ea3065b3eab2c3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/c520bffce576c823d7cbc851198201a820b7f981",
- "reference": "c520bffce576c823d7cbc851198201a820b7f981",
+ "url": "https://api.github.com/repos/Automattic/jetpack-assets/zipball/ca1ebeceeeafb31876a234fa68ea3065b3eab2c3",
+ "reference": "ca1ebeceeeafb31876a234fa68ea3065b3eab2c3",
"shasum": ""
},
"require": {
- "automattic/jetpack-constants": "^2.0.5",
- "php": ">=7.0"
+ "automattic/jetpack-constants": "^3.0.1",
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.8",
- "brain/monkey": "2.6.1",
+ "automattic/jetpack-changelogger": "^5.1.0",
+ "brain/monkey": "^2.6.2",
"wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0",
"yoast/phpunit-polyfills": "^1.1.1"
},
@@ -142,13 +142,13 @@
"type": "jetpack-library",
"extra": {
"autotagger": true,
- "mirror-repo": "Automattic/jetpack-assets",
"textdomain": "jetpack-assets",
+ "mirror-repo": "Automattic/jetpack-assets",
+ "branch-alias": {
+ "dev-trunk": "4.0.x-dev"
+ },
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-assets/compare/v${old}...v${new}"
- },
- "branch-alias": {
- "dev-trunk": "2.3.x-dev"
}
},
"autoload": {
@@ -165,46 +165,46 @@
],
"description": "Asset management utilities for Jetpack ecosystem packages",
"support": {
- "source": "https://github.com/Automattic/jetpack-assets/tree/v2.3.13"
+ "source": "https://github.com/Automattic/jetpack-assets/tree/v4.0.1"
},
- "time": "2024-11-04T09:24:17+00:00"
+ "time": "2024-12-04T19:43:08+00:00"
},
{
"name": "automattic/jetpack-autoloader",
- "version": "v3.0.10",
+ "version": "v5.0.0",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-autoloader.git",
- "reference": "ec4c465ce6a47fb15c15ab0224ec5b1272422d3e"
+ "reference": "eb6331a5c50a03afd9896ce012e66858de9c49c5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/ec4c465ce6a47fb15c15ab0224ec5b1272422d3e",
- "reference": "ec4c465ce6a47fb15c15ab0224ec5b1272422d3e",
+ "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/eb6331a5c50a03afd9896ce012e66858de9c49c5",
+ "reference": "eb6331a5c50a03afd9896ce012e66858de9c49c5",
"shasum": ""
},
"require": {
- "composer-plugin-api": "^1.1 || ^2.0",
- "php": ">=7.0"
+ "composer-plugin-api": "^2.2",
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.6",
- "composer/composer": "^1.1 || ^2.0",
+ "automattic/jetpack-changelogger": "^5.1.0",
+ "composer/composer": "^2.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
"type": "composer-plugin",
"extra": {
- "autotagger": true,
"class": "Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin",
+ "autotagger": true,
"mirror-repo": "Automattic/jetpack-autoloader",
+ "branch-alias": {
+ "dev-trunk": "5.0.x-dev"
+ },
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-autoloader/compare/v${old}...v${new}"
},
"version-constants": {
"::VERSION": "src/AutoloadGenerator.php"
- },
- "branch-alias": {
- "dev-trunk": "3.0.x-dev"
}
},
"autoload": {
@@ -229,29 +229,29 @@
"wordpress"
],
"support": {
- "source": "https://github.com/Automattic/jetpack-autoloader/tree/v3.0.10"
+ "source": "https://github.com/Automattic/jetpack-autoloader/tree/v5.0.0"
},
- "time": "2024-08-26T14:49:14+00:00"
+ "time": "2024-11-25T16:33:57+00:00"
},
{
"name": "automattic/jetpack-config",
- "version": "v2.0.4",
+ "version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-config.git",
- "reference": "9f075c81bae6fd638e0b3183612cda5cc9e01e06"
+ "reference": "fc719eff5073634b0c62793b05be913ca634e192"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-config/zipball/9f075c81bae6fd638e0b3183612cda5cc9e01e06",
- "reference": "9f075c81bae6fd638e0b3183612cda5cc9e01e06",
+ "url": "https://api.github.com/repos/Automattic/jetpack-config/zipball/fc719eff5073634b0c62793b05be913ca634e192",
+ "reference": "fc719eff5073634b0c62793b05be913ca634e192",
"shasum": ""
},
"require": {
- "php": ">=7.0"
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.4",
+ "automattic/jetpack-changelogger": "^5.0.0",
"automattic/jetpack-connection": "@dev",
"automattic/jetpack-import": "@dev",
"automattic/jetpack-jitm": "@dev",
@@ -272,14 +272,14 @@
"type": "jetpack-library",
"extra": {
"autotagger": true,
- "mirror-repo": "Automattic/jetpack-config",
"textdomain": "jetpack-config",
+ "mirror-repo": "Automattic/jetpack-config",
+ "branch-alias": {
+ "dev-trunk": "3.0.x-dev"
+ },
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-config/compare/v${old}...v${new}"
},
- "branch-alias": {
- "dev-trunk": "2.0.x-dev"
- },
"dependencies": {
"test-only": [
"packages/connection",
@@ -309,38 +309,38 @@
],
"description": "Jetpack configuration package that initializes other packages and configures Jetpack's functionality. Can be used as a base for all variants of Jetpack package usage.",
"support": {
- "source": "https://github.com/Automattic/jetpack-config/tree/v2.0.4"
+ "source": "https://github.com/Automattic/jetpack-config/tree/v3.0.0"
},
- "time": "2024-06-24T19:22:07+00:00"
+ "time": "2024-11-14T20:12:40+00:00"
},
{
"name": "automattic/jetpack-connection",
- "version": "v2.12.4",
+ "version": "v6.2.0",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-connection.git",
- "reference": "35dd5b89b9936555ac185e83a489f41655974e70"
+ "reference": "52cd2ba7d845eb516d505959bd9a5e94d1bf4203"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-connection/zipball/35dd5b89b9936555ac185e83a489f41655974e70",
- "reference": "35dd5b89b9936555ac185e83a489f41655974e70",
+ "url": "https://api.github.com/repos/Automattic/jetpack-connection/zipball/52cd2ba7d845eb516d505959bd9a5e94d1bf4203",
+ "reference": "52cd2ba7d845eb516d505959bd9a5e94d1bf4203",
"shasum": ""
},
"require": {
- "automattic/jetpack-a8c-mc-stats": "^2.0.2",
- "automattic/jetpack-admin-ui": "^0.4.3",
- "automattic/jetpack-assets": "^2.3.4",
- "automattic/jetpack-constants": "^2.0.4",
- "automattic/jetpack-redirect": "^2.0.3",
- "automattic/jetpack-roles": "^2.0.3",
- "automattic/jetpack-status": "^3.3.4",
- "php": ">=7.0"
+ "automattic/jetpack-a8c-mc-stats": "^3.0.0",
+ "automattic/jetpack-admin-ui": "^0.5.1",
+ "automattic/jetpack-assets": "^4.0.1",
+ "automattic/jetpack-constants": "^3.0.1",
+ "automattic/jetpack-redirect": "^3.0.1",
+ "automattic/jetpack-roles": "^3.0.1",
+ "automattic/jetpack-status": "^5.0.1",
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.6",
- "automattic/wordbless": "@dev",
- "brain/monkey": "2.6.1",
+ "automattic/jetpack-changelogger": "^5.1.0",
+ "automattic/wordbless": "^0.4.2",
+ "brain/monkey": "^2.6.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -349,25 +349,28 @@
"type": "jetpack-library",
"extra": {
"autotagger": true,
- "mirror-repo": "Automattic/jetpack-connection",
"textdomain": "jetpack-connection",
- "version-constants": {
- "::PACKAGE_VERSION": "src/class-package-version.php"
+ "mirror-repo": "Automattic/jetpack-connection",
+ "branch-alias": {
+ "dev-trunk": "6.2.x-dev"
},
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-connection/compare/v${old}...v${new}"
},
- "branch-alias": {
- "dev-trunk": "2.12.x-dev"
- },
"dependencies": {
"test-only": [
"packages/licensing",
"packages/sync"
]
+ },
+ "version-constants": {
+ "::PACKAGE_VERSION": "src/class-package-version.php"
}
},
"autoload": {
+ "files": [
+ "actions.php"
+ ],
"classmap": [
"legacy",
"src/",
@@ -381,30 +384,30 @@
],
"description": "Everything needed to connect to the Jetpack infrastructure",
"support": {
- "source": "https://github.com/Automattic/jetpack-connection/tree/v2.12.4"
+ "source": "https://github.com/Automattic/jetpack-connection/tree/v6.2.0"
},
- "time": "2024-08-23T14:29:32+00:00"
+ "time": "2024-12-09T15:47:56+00:00"
},
{
"name": "automattic/jetpack-constants",
- "version": "v2.0.5",
+ "version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-constants.git",
- "reference": "0c2644d642b06ae2a31c561f5bfc6f74a4abc8f1"
+ "reference": "d4b7820defcdb40c1add88d5ebd722e4ba80a873"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/0c2644d642b06ae2a31c561f5bfc6f74a4abc8f1",
- "reference": "0c2644d642b06ae2a31c561f5bfc6f74a4abc8f1",
+ "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/d4b7820defcdb40c1add88d5ebd722e4ba80a873",
+ "reference": "d4b7820defcdb40c1add88d5ebd722e4ba80a873",
"shasum": ""
},
"require": {
- "php": ">=7.0"
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.8",
- "brain/monkey": "2.6.1",
+ "automattic/jetpack-changelogger": "^5.1.0",
+ "brain/monkey": "^2.6.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -414,11 +417,11 @@
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-constants",
+ "branch-alias": {
+ "dev-trunk": "3.0.x-dev"
+ },
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-constants/compare/v${old}...v${new}"
- },
- "branch-alias": {
- "dev-trunk": "2.0.x-dev"
}
},
"autoload": {
@@ -432,30 +435,30 @@
],
"description": "A wrapper for defining constants in a more testable way.",
"support": {
- "source": "https://github.com/Automattic/jetpack-constants/tree/v2.0.5"
+ "source": "https://github.com/Automattic/jetpack-constants/tree/v3.0.1"
},
- "time": "2024-11-04T09:23:35+00:00"
+ "time": "2024-11-25T16:33:27+00:00"
},
{
"name": "automattic/jetpack-ip",
- "version": "v0.2.3",
+ "version": "v0.4.1",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-ip.git",
- "reference": "f7a42b1603a24775c6f20eef2ac5cba3d6b37194"
+ "reference": "04d7deb2c16faa6c4a3e5074bf0e12c8a87d035a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-ip/zipball/f7a42b1603a24775c6f20eef2ac5cba3d6b37194",
- "reference": "f7a42b1603a24775c6f20eef2ac5cba3d6b37194",
+ "url": "https://api.github.com/repos/Automattic/jetpack-ip/zipball/04d7deb2c16faa6c4a3e5074bf0e12c8a87d035a",
+ "reference": "04d7deb2c16faa6c4a3e5074bf0e12c8a87d035a",
"shasum": ""
},
"require": {
- "php": ">=7.0"
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.6",
- "brain/monkey": "2.6.1",
+ "automattic/jetpack-changelogger": "^5.1.0",
+ "brain/monkey": "^2.6.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -464,14 +467,14 @@
"type": "jetpack-library",
"extra": {
"autotagger": true,
+ "textdomain": "jetpack-ip",
"mirror-repo": "Automattic/jetpack-ip",
+ "branch-alias": {
+ "dev-trunk": "0.4.x-dev"
+ },
"changelogger": {
"link-template": "https://github.com/automattic/jetpack-ip/compare/v${old}...v${new}"
},
- "branch-alias": {
- "dev-trunk": "0.2.x-dev"
- },
- "textdomain": "jetpack-ip",
"version-constants": {
"::PACKAGE_VERSION": "src/class-utils.php"
}
@@ -487,30 +490,30 @@
],
"description": "Utilities for working with IP addresses.",
"support": {
- "source": "https://github.com/Automattic/jetpack-ip/tree/v0.2.3"
+ "source": "https://github.com/Automattic/jetpack-ip/tree/v0.4.1"
},
- "time": "2024-08-23T14:28:05+00:00"
+ "time": "2024-11-25T16:33:22+00:00"
},
{
"name": "automattic/jetpack-password-checker",
- "version": "v0.3.3",
+ "version": "v0.4.1",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-password-checker.git",
- "reference": "1812a38452575e7c8c7c06affeeca776a367225f"
+ "reference": "e721e7659cc7a6a37152a4e96485e6c139f02d5f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-password-checker/zipball/1812a38452575e7c8c7c06affeeca776a367225f",
- "reference": "1812a38452575e7c8c7c06affeeca776a367225f",
+ "url": "https://api.github.com/repos/Automattic/jetpack-password-checker/zipball/e721e7659cc7a6a37152a4e96485e6c139f02d5f",
+ "reference": "e721e7659cc7a6a37152a4e96485e6c139f02d5f",
"shasum": ""
},
"require": {
- "php": ">=7.0"
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.8",
- "automattic/wordbless": "@dev",
+ "automattic/jetpack-changelogger": "^5.1.0",
+ "automattic/wordbless": "^0.4.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -519,13 +522,13 @@
"type": "jetpack-library",
"extra": {
"autotagger": true,
- "mirror-repo": "Automattic/jetpack-password-checker",
"textdomain": "jetpack-password-checker",
+ "mirror-repo": "Automattic/jetpack-password-checker",
+ "branch-alias": {
+ "dev-trunk": "0.4.x-dev"
+ },
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-password-checker/compare/v${old}...v${new}"
- },
- "branch-alias": {
- "dev-trunk": "0.3.x-dev"
}
},
"autoload": {
@@ -539,31 +542,31 @@
],
"description": "Password Checker.",
"support": {
- "source": "https://github.com/Automattic/jetpack-password-checker/tree/v0.3.3"
+ "source": "https://github.com/Automattic/jetpack-password-checker/tree/v0.4.1"
},
- "time": "2024-11-04T09:23:39+00:00"
+ "time": "2024-11-25T16:33:31+00:00"
},
{
"name": "automattic/jetpack-redirect",
- "version": "v2.0.3",
+ "version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-redirect.git",
- "reference": "2c049bb08f736dc0dbafac7eaebea6f97cf8019e"
+ "reference": "89732a3ba1c5eba8cfd948b7567823cd884102d5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/2c049bb08f736dc0dbafac7eaebea6f97cf8019e",
- "reference": "2c049bb08f736dc0dbafac7eaebea6f97cf8019e",
+ "url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/89732a3ba1c5eba8cfd948b7567823cd884102d5",
+ "reference": "89732a3ba1c5eba8cfd948b7567823cd884102d5",
"shasum": ""
},
"require": {
- "automattic/jetpack-status": "^3.3.4",
- "php": ">=7.0"
+ "automattic/jetpack-status": "^5.0.1",
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.6",
- "brain/monkey": "2.6.1",
+ "automattic/jetpack-changelogger": "^5.1.0",
+ "brain/monkey": "^2.6.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -573,11 +576,11 @@
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-redirect",
+ "branch-alias": {
+ "dev-trunk": "3.0.x-dev"
+ },
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-redirect/compare/v${old}...v${new}"
- },
- "branch-alias": {
- "dev-trunk": "2.0.x-dev"
}
},
"autoload": {
@@ -591,30 +594,30 @@
],
"description": "Utilities to build URLs to the jetpack.com/redirect/ service",
"support": {
- "source": "https://github.com/Automattic/jetpack-redirect/tree/v2.0.3"
+ "source": "https://github.com/Automattic/jetpack-redirect/tree/v3.0.1"
},
- "time": "2024-08-23T14:28:46+00:00"
+ "time": "2024-11-25T16:34:01+00:00"
},
{
"name": "automattic/jetpack-roles",
- "version": "v2.0.4",
+ "version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-roles.git",
- "reference": "2fa5361ce8ff271cc4ecfac5be9b957ab0e9912f"
+ "reference": "fe5f2a45901ea14be00728119d097619615fb031"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/2fa5361ce8ff271cc4ecfac5be9b957ab0e9912f",
- "reference": "2fa5361ce8ff271cc4ecfac5be9b957ab0e9912f",
+ "url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/fe5f2a45901ea14be00728119d097619615fb031",
+ "reference": "fe5f2a45901ea14be00728119d097619615fb031",
"shasum": ""
},
"require": {
- "php": ">=7.0"
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.8",
- "brain/monkey": "2.6.1",
+ "automattic/jetpack-changelogger": "^5.1.0",
+ "brain/monkey": "^2.6.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -624,11 +627,11 @@
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-roles",
+ "branch-alias": {
+ "dev-trunk": "3.0.x-dev"
+ },
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-roles/compare/v${old}...v${new}"
- },
- "branch-alias": {
- "dev-trunk": "2.0.x-dev"
}
},
"autoload": {
@@ -642,34 +645,34 @@
],
"description": "Utilities, related with user roles and capabilities.",
"support": {
- "source": "https://github.com/Automattic/jetpack-roles/tree/v2.0.4"
+ "source": "https://github.com/Automattic/jetpack-roles/tree/v3.0.1"
},
- "time": "2024-11-04T09:23:38+00:00"
+ "time": "2024-11-25T16:33:29+00:00"
},
{
"name": "automattic/jetpack-status",
- "version": "v3.3.5",
+ "version": "v5.0.1",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-status.git",
- "reference": "69d5d8a8f31adf2b297a539bcddd9a9162d1320b"
+ "reference": "769f55b6327187a85c14ed21943eea430f63220d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/69d5d8a8f31adf2b297a539bcddd9a9162d1320b",
- "reference": "69d5d8a8f31adf2b297a539bcddd9a9162d1320b",
+ "url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/769f55b6327187a85c14ed21943eea430f63220d",
+ "reference": "769f55b6327187a85c14ed21943eea430f63220d",
"shasum": ""
},
"require": {
- "automattic/jetpack-constants": "^2.0.4",
- "php": ">=7.0"
+ "automattic/jetpack-constants": "^3.0.1",
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.6",
+ "automattic/jetpack-changelogger": "^5.1.0",
"automattic/jetpack-connection": "@dev",
- "automattic/jetpack-ip": "^0.2.3",
+ "automattic/jetpack-ip": "^0.4.1",
"automattic/jetpack-plans": "@dev",
- "brain/monkey": "2.6.1",
+ "brain/monkey": "^2.6.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -679,12 +682,12 @@
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-status",
+ "branch-alias": {
+ "dev-trunk": "5.0.x-dev"
+ },
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-status/compare/v${old}...v${new}"
},
- "branch-alias": {
- "dev-trunk": "3.3.x-dev"
- },
"dependencies": {
"test-only": [
"packages/connection",
@@ -703,38 +706,38 @@
],
"description": "Used to retrieve information about the current status of Jetpack and the site overall.",
"support": {
- "source": "https://github.com/Automattic/jetpack-status/tree/v3.3.5"
+ "source": "https://github.com/Automattic/jetpack-status/tree/v5.0.1"
},
- "time": "2024-09-10T17:55:40+00:00"
+ "time": "2024-11-25T16:33:53+00:00"
},
{
"name": "automattic/jetpack-sync",
- "version": "v3.8.0",
+ "version": "v4.1.0",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-sync.git",
- "reference": "30b29f0c5a27e01cbf2fa592fbde97f617665153"
+ "reference": "5747f144575b9474622692f2bc8e4315363ea44d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Automattic/jetpack-sync/zipball/30b29f0c5a27e01cbf2fa592fbde97f617665153",
- "reference": "30b29f0c5a27e01cbf2fa592fbde97f617665153",
+ "url": "https://api.github.com/repos/Automattic/jetpack-sync/zipball/5747f144575b9474622692f2bc8e4315363ea44d",
+ "reference": "5747f144575b9474622692f2bc8e4315363ea44d",
"shasum": ""
},
"require": {
- "automattic/jetpack-connection": "^2.12.4",
- "automattic/jetpack-constants": "^2.0.4",
- "automattic/jetpack-ip": "^0.2.3",
- "automattic/jetpack-password-checker": "^0.3.2",
- "automattic/jetpack-roles": "^2.0.3",
- "automattic/jetpack-status": "^3.3.4",
- "php": ">=7.0"
+ "automattic/jetpack-connection": "^6.2.0",
+ "automattic/jetpack-constants": "^3.0.1",
+ "automattic/jetpack-ip": "^0.4.1",
+ "automattic/jetpack-password-checker": "^0.4.1",
+ "automattic/jetpack-roles": "^3.0.1",
+ "automattic/jetpack-status": "^5.0.1",
+ "php": ">=7.2"
},
"require-dev": {
- "automattic/jetpack-changelogger": "^4.2.6",
+ "automattic/jetpack-changelogger": "^5.1.0",
"automattic/jetpack-search": "@dev",
- "automattic/jetpack-waf": "^0.18.4",
- "automattic/wordbless": "@dev",
+ "automattic/jetpack-waf": "^0.23.1",
+ "automattic/wordbless": "^0.4.2",
"yoast/phpunit-polyfills": "^1.1.1"
},
"suggest": {
@@ -743,22 +746,22 @@
"type": "jetpack-library",
"extra": {
"autotagger": true,
- "mirror-repo": "Automattic/jetpack-sync",
"textdomain": "jetpack-sync",
- "version-constants": {
- "::PACKAGE_VERSION": "src/class-package-version.php"
+ "mirror-repo": "Automattic/jetpack-sync",
+ "branch-alias": {
+ "dev-trunk": "4.1.x-dev"
},
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-sync/compare/v${old}...v${new}"
},
- "branch-alias": {
- "dev-trunk": "3.8.x-dev"
- },
"dependencies": {
"test-only": [
"packages/search",
"packages/waf"
]
+ },
+ "version-constants": {
+ "::PACKAGE_VERSION": "src/class-package-version.php"
}
},
"autoload": {
@@ -772,9 +775,9 @@
],
"description": "Everything needed to allow syncing to the WP.com infrastructure.",
"support": {
- "source": "https://github.com/Automattic/jetpack-sync/tree/v3.8.0"
+ "source": "https://github.com/Automattic/jetpack-sync/tree/v4.1.0"
},
- "time": "2024-08-26T14:49:56+00:00"
+ "time": "2024-12-09T15:48:10+00:00"
},
{
"name": "composer/installers",
diff --git a/dev/phpcs/WCPay/ruleset.xml b/dev/phpcs/WCPay/ruleset.xml
index 9806ccfe9e7..7c8cefbd0e3 100644
--- a/dev/phpcs/WCPay/ruleset.xml
+++ b/dev/phpcs/WCPay/ruleset.xml
@@ -17,10 +17,6 @@
*/includes/class-wc-payments-order-success-page.php
-
-
*/includes/class-wc-payments-customer-service.php
-
*/includes/class-wc-payments-token-service.php
-
*/includes/class-wc-payments-webhook-reliability-service.php
diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php
index 01cce6c775e..d78671d1298 100644
--- a/includes/admin/class-wc-payments-admin.php
+++ b/includes/admin/class-wc-payments-admin.php
@@ -6,6 +6,7 @@
*/
use Automattic\Jetpack\Identity_Crisis as Jetpack_Identity_Crisis;
+use WCPay\Constants\Intent_Status;
use WCPay\Core\Server\Request;
use WCPay\Database_Cache;
use WCPay\Logger;
@@ -144,49 +145,6 @@ public function __construct(
$this->incentives_service = $incentives_service;
$this->fraud_service = $fraud_service;
$this->database_cache = $database_cache;
-
- $this->admin_child_pages = [
- 'wc-payments-overview' => [
- 'id' => 'wc-payments-overview',
- 'title' => __( 'Overview', 'woocommerce-payments' ),
- 'parent' => 'wc-payments',
- 'path' => '/payments/overview',
- 'nav_args' => [
- 'parent' => 'wc-payments',
- 'order' => 10,
- ],
- ],
- 'wc-payments-deposits' => [
- 'id' => 'wc-payments-deposits',
- 'title' => __( 'Payouts', 'woocommerce-payments' ),
- 'parent' => 'wc-payments',
- 'path' => '/payments/payouts',
- 'nav_args' => [
- 'parent' => 'wc-payments',
- 'order' => 20,
- ],
- ],
- 'wc-payments-transactions' => [
- 'id' => 'wc-payments-transactions',
- 'title' => __( 'Transactions', 'woocommerce-payments' ),
- 'parent' => 'wc-payments',
- 'path' => '/payments/transactions',
- 'nav_args' => [
- 'parent' => 'wc-payments',
- 'order' => 30,
- ],
- ],
- 'wc-payments-disputes' => [
- 'id' => 'wc-payments-disputes',
- 'title' => __( 'Disputes', 'woocommerce-payments' ),
- 'parent' => 'wc-payments',
- 'path' => '/payments/disputes',
- 'nav_args' => [
- 'parent' => 'wc-payments',
- 'order' => 40,
- ],
- ],
- ];
}
/**
@@ -315,6 +273,49 @@ public function add_payments_menu() {
}
global $submenu;
+ $this->admin_child_pages = [
+ 'wc-payments-overview' => [
+ 'id' => 'wc-payments-overview',
+ 'title' => __( 'Overview', 'woocommerce-payments' ),
+ 'parent' => 'wc-payments',
+ 'path' => '/payments/overview',
+ 'nav_args' => [
+ 'parent' => 'wc-payments',
+ 'order' => 10,
+ ],
+ ],
+ 'wc-payments-deposits' => [
+ 'id' => 'wc-payments-deposits',
+ 'title' => __( 'Payouts', 'woocommerce-payments' ),
+ 'parent' => 'wc-payments',
+ 'path' => '/payments/payouts',
+ 'nav_args' => [
+ 'parent' => 'wc-payments',
+ 'order' => 20,
+ ],
+ ],
+ 'wc-payments-transactions' => [
+ 'id' => 'wc-payments-transactions',
+ 'title' => __( 'Transactions', 'woocommerce-payments' ),
+ 'parent' => 'wc-payments',
+ 'path' => '/payments/transactions',
+ 'nav_args' => [
+ 'parent' => 'wc-payments',
+ 'order' => 30,
+ ],
+ ],
+ 'wc-payments-disputes' => [
+ 'id' => 'wc-payments-disputes',
+ 'title' => __( 'Disputes', 'woocommerce-payments' ),
+ 'parent' => 'wc-payments',
+ 'path' => '/payments/disputes',
+ 'nav_args' => [
+ 'parent' => 'wc-payments',
+ 'order' => 40,
+ ],
+ ],
+ ];
+
try {
// Render full payments menu with sub-items only if:
// - we have working WPCOM/Jetpack connection;
@@ -1253,7 +1254,7 @@ public function show_woopay_payment_method_name_admin( $order_id ) {
*/
public function display_wcpay_transaction_fee( $order_id ) {
$order = wc_get_order( $order_id );
- if ( ! $order || ! $order->get_meta( '_wcpay_transaction_fee' ) ) {
+ if ( ! $order || ! $order->get_meta( '_wcpay_transaction_fee' ) || Intent_Status::REQUIRES_CAPTURE === $order->get_meta( WC_Payments_Order_Service::INTENTION_STATUS_META_KEY ) ) {
return;
}
?>
@@ -1336,6 +1337,7 @@ public function add_transactions_notification_badge() {
/**
* Gets the number of disputes which need a response. ie have a 'needs_response' or 'warning_needs_response' status.
+ * Used to display a notification badge on the Payments > Disputes menu item.
*
* @return int The number of disputes which need a response.
*/
diff --git a/includes/admin/tasks/class-wc-payments-task-disputes.php b/includes/admin/tasks/class-wc-payments-task-disputes.php
index b7212ec7623..7d5ac82faf4 100644
--- a/includes/admin/tasks/class-wc-payments-task-disputes.php
+++ b/includes/admin/tasks/class-wc-payments-task-disputes.php
@@ -49,6 +49,13 @@ class WC_Payments_Task_Disputes extends Task {
*/
private $disputes_due_within_1d;
+ /**
+ * A memory cache of all disputes needing response.
+ *
+ * @var array|null
+ */
+ private $disputes_needing_response = null;
+
/**
* WC_Payments_Task_Disputes constructor.
*/
@@ -57,13 +64,12 @@ public function __construct() {
$this->api_client = \WC_Payments::get_payments_api_client();
$this->database_cache = \WC_Payments::get_database_cache();
parent::__construct();
- $this->init();
}
/**
* Initialize the task.
*/
- private function init() {
+ private function fetch_relevant_disputes() {
$this->disputes_due_within_7d = $this->get_disputes_needing_response_within_days( 7 );
$this->disputes_due_within_1d = $this->get_disputes_needing_response_within_days( 1 );
}
@@ -83,6 +89,9 @@ public function get_id() {
* @return string
*/
public function get_title() {
+ if ( null === $this->disputes_needing_response ) {
+ $this->fetch_relevant_disputes();
+ }
if ( count( (array) $this->disputes_due_within_7d ) === 1 ) {
$dispute = $this->disputes_due_within_7d[0];
$amount = WC_Payments_Utils::interpret_stripe_amount( $dispute['amount'], $dispute['currency'] );
@@ -275,6 +284,9 @@ public function is_complete() {
* @return bool
*/
public function can_view() {
+ if ( null === $this->disputes_needing_response ) {
+ $this->fetch_relevant_disputes();
+ }
return count( (array) $this->disputes_due_within_7d ) > 0;
}
@@ -322,15 +334,24 @@ private function get_disputes_needing_response_within_days( $num_days ) {
* @return array|null Array of disputes awaiting a response. Null on failure.
*/
private function get_disputes_needing_response() {
- return $this->database_cache->get_or_add(
+ if ( null !== $this->disputes_needing_response ) {
+ return $this->disputes_needing_response;
+ }
+
+ $this->disputes_needing_response = $this->database_cache->get_or_add(
Database_Cache::ACTIVE_DISPUTES_KEY,
function () {
- $response = $this->api_client->get_disputes(
- [
- 'pagesize' => 50,
- 'search' => [ 'warning_needs_response', 'needs_response' ],
- ]
- );
+ try {
+ $response = $this->api_client->get_disputes(
+ [
+ 'pagesize' => 50,
+ 'search' => [ 'warning_needs_response', 'needs_response' ],
+ ]
+ );
+ } catch ( \Exception $e ) {
+ // Ensure an array is always returned, even if the API call fails.
+ return [];
+ }
$active_disputes = $response['data'] ?? [];
@@ -347,8 +368,9 @@ function ( $a, $b ) {
return $active_disputes;
},
- // We'll consider all array values to be valid as the cache is only invalidated when it is deleted or it expires.
'is_array'
);
+
+ return $this->disputes_needing_response;
}
}
diff --git a/includes/class-database-cache.php b/includes/class-database-cache.php
index 1e285be59ab..e29bdbfc374 100644
--- a/includes/class-database-cache.php
+++ b/includes/class-database-cache.php
@@ -20,6 +20,7 @@ class Database_Cache implements MultiCurrencyCacheInterface {
const BUSINESS_TYPES_KEY = 'wcpay_business_types_data';
const PAYMENT_PROCESS_FACTORS_KEY = 'wcpay_payment_process_factors';
const FRAUD_SERVICES_KEY = 'wcpay_fraud_services_data';
+ const RECOMMENDED_PAYMENT_METHODS = 'wcpay_recommended_payment_methods';
/**
* Refresh during AJAX calls is avoided, but white-listing
diff --git a/includes/class-logger.php b/includes/class-logger.php
index 3384d0fe443..1ce8ae255ab 100644
--- a/includes/class-logger.php
+++ b/includes/class-logger.php
@@ -36,7 +36,7 @@ class Logger {
* 'debug': Debug-level messages.
*/
public static function log( $message, $level = 'info' ) {
- wcpay_get_container()->get( InternalLogger::class )->log( $message );
+ wcpay_get_container()->get( InternalLogger::class )->log( $message, $level );
}
/**
diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php
index 0eca255b168..4c1a26a1d60 100644
--- a/includes/class-wc-payment-gateway-wcpay.php
+++ b/includes/class-wc-payment-gateway-wcpay.php
@@ -20,7 +20,19 @@
use WCPay\Constants\Intent_Status;
use WCPay\Constants\Payment_Type;
use WCPay\Constants\Payment_Method;
-use WCPay\Exceptions\{ Add_Payment_Method_Exception, Amount_Too_Small_Exception, Process_Payment_Exception, Intent_Authentication_Exception, API_Exception, Invalid_Address_Exception, Fraud_Prevention_Enabled_Exception, Invalid_Phone_Number_Exception, Rate_Limiter_Enabled_Exception, Order_ID_Mismatch_Exception, Order_Not_Found_Exception, New_Process_Payment_Exception };
+use WCPay\Exceptions\{Add_Payment_Method_Exception,
+ Amount_Too_Small_Exception,
+ API_Merchant_Exception,
+ Process_Payment_Exception,
+ Intent_Authentication_Exception,
+ API_Exception,
+ Invalid_Address_Exception,
+ Fraud_Prevention_Enabled_Exception,
+ Invalid_Phone_Number_Exception,
+ Rate_Limiter_Enabled_Exception,
+ Order_ID_Mismatch_Exception,
+ Order_Not_Found_Exception,
+ New_Process_Payment_Exception};
use WCPay\Core\Server\Request\Cancel_Intention;
use WCPay\Core\Server\Request\Capture_Intention;
use WCPay\Core\Server\Request\Create_And_Confirm_Intention;
@@ -309,13 +321,11 @@ public function __construct(
$this->fraud_service = $fraud_service;
$this->duplicate_payment_methods_detection_service = $duplicate_payment_methods_detection_service;
- $this->id = static::GATEWAY_ID;
- $this->icon = $this->get_theme_icon();
- $this->has_fields = true;
- $this->method_title = 'WooPayments';
- $this->method_description = $this->get_method_description();
+ $this->id = static::GATEWAY_ID;
+ $this->icon = $this->get_theme_icon();
+ $this->has_fields = true;
+ $this->method_title = 'WooPayments';
- $this->title = $payment_method->get_title();
$this->description = '';
$this->supports = [
'products',
@@ -323,11 +333,61 @@ public function __construct(
];
if ( 'card' !== $this->stripe_id ) {
- $this->id = self::GATEWAY_ID . '_' . $this->stripe_id;
- $this->method_title = "WooPayments ($this->title)";
+ $this->id = self::GATEWAY_ID . '_' . $this->stripe_id;
}
- // Define setting fields.
+ // Capabilities have different keys than the payment method ID's,
+ // so instead of appending '_payments' to the end of the ID, it'll be better
+ // to have a map for it instead, just in case the pattern changes.
+ $this->payment_method_capability_key_map = [
+ 'sofort' => 'sofort_payments',
+ 'giropay' => 'giropay_payments',
+ 'bancontact' => 'bancontact_payments',
+ 'eps' => 'eps_payments',
+ 'ideal' => 'ideal_payments',
+ 'p24' => 'p24_payments',
+ 'card' => 'card_payments',
+ 'sepa_debit' => 'sepa_debit_payments',
+ 'au_becs_debit' => 'au_becs_debit_payments',
+ 'link' => 'link_payments',
+ 'affirm' => 'affirm_payments',
+ 'afterpay_clearpay' => 'afterpay_clearpay_payments',
+ 'klarna' => 'klarna_payments',
+ 'jcb' => 'jcb_payments',
+ ];
+
+ // WooPay utilities.
+ $this->woopay_util = new WooPay_Utilities();
+
+ // Load the settings.
+ $this->init_settings();
+
+ // Check if subscriptions are enabled and add support for them.
+ $this->maybe_init_subscriptions();
+
+ // If the setting to enable saved cards is enabled, then we should support tokenization and adding payment methods.
+ if ( $this->is_saved_cards_enabled() ) {
+ array_push( $this->supports, 'tokenization', 'add_payment_method' );
+ }
+ }
+
+ /**
+ * Return the gateway's title.
+ *
+ * @return string
+ */
+ public function get_title() {
+ $this->title = $this->payment_method->get_title();
+ $this->method_title = "WooPayments ($this->title)";
+ return parent::get_title();
+ }
+
+ /**
+ * Get the form fields after they are initialized.
+ *
+ * @return array of options
+ */
+ public function get_form_fields() {
$this->form_fields = [
'enabled' => [
'title' => __( 'Enable/disable', 'woocommerce-payments' ),
@@ -497,39 +557,7 @@ public function __construct(
'platform_checkout_custom_message' => [ 'default' => __( 'By placing this order, you agree to our [terms] and understand our [privacy_policy].', 'woocommerce-payments' ) ],
];
- // Capabilities have different keys than the payment method ID's,
- // so instead of appending '_payments' to the end of the ID, it'll be better
- // to have a map for it instead, just in case the pattern changes.
- $this->payment_method_capability_key_map = [
- 'sofort' => 'sofort_payments',
- 'giropay' => 'giropay_payments',
- 'bancontact' => 'bancontact_payments',
- 'eps' => 'eps_payments',
- 'ideal' => 'ideal_payments',
- 'p24' => 'p24_payments',
- 'card' => 'card_payments',
- 'sepa_debit' => 'sepa_debit_payments',
- 'au_becs_debit' => 'au_becs_debit_payments',
- 'link' => 'link_payments',
- 'affirm' => 'affirm_payments',
- 'afterpay_clearpay' => 'afterpay_clearpay_payments',
- 'klarna' => 'klarna_payments',
- 'jcb' => 'jcb_payments',
- ];
-
- // WooPay utilities.
- $this->woopay_util = new WooPay_Utilities();
-
- // Load the settings.
- $this->init_settings();
-
- // Check if subscriptions are enabled and add support for them.
- $this->maybe_init_subscriptions();
-
- // If the setting to enable saved cards is enabled, then we should support tokenization and adding payment methods.
- if ( $this->is_saved_cards_enabled() ) {
- array_push( $this->supports, 'tokenization', 'add_payment_method' );
- }
+ return parent::get_form_fields();
}
/**
@@ -1254,6 +1282,9 @@ public function process_payment( $order_id ) {
);
$error_details = esc_html( rtrim( $e->getMessage(), '.' ) );
+ if ( $e instanceof API_Merchant_Exception ) {
+ $error_details = $error_details . '. ' . esc_html( rtrim( $e->get_merchant_message(), '.' ) );
+ }
if ( $e instanceof API_Exception && 'card_error' === $e->get_error_type() ) {
// If the payment failed with a 'card_error' API exception, initialize the fraud meta box
@@ -1571,7 +1602,6 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
throw new Exception( WC_Payments_Utils::get_filtered_error_message( $e ) );
}
- $payment_methods = $this->get_payment_method_types( $payment_information );
// The sanitize_user call here is deliberate: it seems the most appropriate sanitization function
// for a string that will only contain latin alphanumeric characters and underscores.
// phpcs:ignore WordPress.Security.NonceVerification.Missing
@@ -1602,6 +1632,8 @@ public function process_payment_for_order( $cart, $payment_information, $schedul
}
if ( empty( $intent ) ) {
+ $payment_methods = $this->get_payment_method_types( $payment_information );
+
$request = Create_And_Confirm_Intention::create();
$request->set_amount( $converted_amount );
$request->set_currency_code( $currency );
@@ -2126,9 +2158,6 @@ public function get_payment_method_types( $payment_information ): array {
$order = $payment_information->get_order();
$order_id = $order instanceof WC_Order ? $order->get_id() : null;
$payment_methods = $this->get_payment_methods_from_gateway_id( $token->get_gateway_id(), $order_id );
- } else {
- // Final fallback case, if all else fails.
- $payment_methods = WC_Payments::get_gateway()->get_payment_method_ids_enabled_at_checkout( null, true );
}
return $payment_methods;
@@ -3375,7 +3404,7 @@ public function capture_charge( $order, $include_level3 = true, $intent_metadata
$this->attach_exchange_info_to_order( $order, $charge_id );
if ( Intent_Status::SUCCEEDED === $status ) {
- $this->order_service->update_order_status_from_intent( $order, $intent );
+ $this->order_service->process_captured_payment( $order, $intent );
} elseif ( $is_authorization_expired ) {
$this->order_service->mark_payment_capture_expired( $order, $intent_id, Intent_Status::CANCELED, $charge_id );
} else {
@@ -4499,11 +4528,40 @@ public function find_duplicates() {
return $this->duplicate_payment_methods_detection_service->find_duplicates();
}
+ /**
+ * Get the recommended payment methods list.
+ *
+ * @param string $country_code Optional. The business location country code. Provide a 2-letter ISO country code.
+ * If not provided, the account country will be used if the account is connected.
+ * Otherwise, the store's base country will be used.
+ *
+ * @return array List of recommended payment methods for the given country.
+ * Empty array if there are no recommendations available.
+ * Each item in the array should be an associative array with at least the following entries:
+ * - @string id: The payment method ID.
+ * - @string title: The payment method title/name.
+ * - @bool enabled: Whether the payment method is enabled.
+ * - @int order/priority: The order/priority of the payment method.
+ */
+ public function get_recommended_payment_methods( string $country_code = '' ): array {
+ if ( empty( $country_code ) ) {
+ // If the account is connected, use the account country.
+ if ( $this->account->is_provider_connected() ) {
+ $country_code = $this->get_account_country();
+ } else {
+ // If the account is not connected, use the store's base country.
+ $country_code = WC()->countries->get_base_country();
+ }
+ }
+
+ return $this->account->get_recommended_payment_methods( $country_code );
+ }
+
/**
* Determine whether redirection is needed for the non-card UPE payment method.
*
* @param array $payment_methods The list of payment methods used for the order processing, usually consists of one method only.
- * @return boolean True if the arrray consist of only one payment method which is not a card. False otherwise.
+ * @return boolean True if the array consist of only one payment method which is not a card. False otherwise.
*/
private function upe_needs_redirection( $payment_methods ) {
return 1 === count( $payment_methods ) && 'card' !== $payment_methods[0];
diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php
index e884d582ac8..dc5430d6f0f 100644
--- a/includes/class-wc-payments-account.php
+++ b/includes/class-wc-payments-account.php
@@ -30,6 +30,7 @@ class WC_Payments_Account implements MultiCurrencyAccountInterface {
const ONBOARDING_STARTED_TRANSIENT = 'wcpay_on_boarding_started';
const ONBOARDING_STATE_TRANSIENT = 'wcpay_stripe_onboarding_state';
const WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT = 'woopay_enabled_by_default';
+ const ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT = 'test_drive_account_settings_for_live_account';
const EMBEDDED_KYC_IN_PROGRESS_OPTION = 'wcpay_onboarding_embedded_kyc_in_progress';
const ERROR_MESSAGE_TRANSIENT = 'wcpay_error_message';
const INSTANT_DEPOSITS_REMINDER_ACTION = 'wcpay_instant_deposit_reminder';
@@ -665,6 +666,69 @@ public function get_supported_countries(): array {
return WC_Payments_Utils::supported_countries();
}
+ /**
+ * Get the account recommended payment methods to use during onboarding.
+ *
+ * @param string $country_code The account's business location country code. Provide a 2-letter ISO country code.
+ *
+ * @return array List of recommended payment methods for the given country.
+ * Empty array if there are no recommendations, we failed to retrieve recommendations,
+ * or the country is not supported by WooPayments.
+ */
+ public function get_recommended_payment_methods( string $country_code ): array {
+ // Return early if the country is not supported.
+ if ( ! array_key_exists( $country_code, $this->get_supported_countries() ) ) {
+ return [];
+ }
+
+ // We use the locale for the current user (defaults to the site locale).
+ $recommended_pms = $this->onboarding_service->get_recommended_payment_methods( $country_code, get_user_locale() );
+ $recommended_pms = is_array( $recommended_pms ) ? array_values( $recommended_pms ) : [];
+
+ // Validate the recommended payment methods.
+ // Each must have an ID and a title.
+ $recommended_pms = array_filter(
+ $recommended_pms,
+ function ( $pm ) {
+ return isset( $pm['id'] ) && isset( $pm['title'] );
+ }
+ );
+
+ // Standardize/normalize.
+ // Determine if the payment method should be recommended as enabled.
+ $recommended_pms = array_map(
+ function ( $pm ) {
+ if ( ! isset( $pm['enabled'] ) ) {
+ // Default to enabled since this is a recommended list.
+ $pm['enabled'] = true;
+ // Look at the type, if available, to determine if it should be enabled.
+ if ( isset( $pm['type'] ) ) {
+ $pm['enabled'] = 'available' !== $pm['type'];
+ }
+ }
+
+ return $pm;
+ },
+ $recommended_pms
+ );
+ // Fill in the priority entries with a fallback to the index of the recommendation in the list.
+ $recommended_pms = array_map(
+ function ( $pm, $index ) {
+ if ( ! isset( $pm['priority'] ) ) {
+ $pm['priority'] = $index;
+ } else {
+ $pm['priority'] = intval( $pm['priority'] );
+ }
+
+ return $pm;
+ },
+ $recommended_pms,
+ array_keys( $recommended_pms )
+ );
+
+ return $recommended_pms;
+ }
+
/**
* Gets the account live mode value.
*
@@ -1254,6 +1318,7 @@ public function maybe_handle_onboarding() {
}
$this->cleanup_on_account_reset();
+ delete_transient( self::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT );
// When we reset the account and want to go back to the settings page - redirect immediately!
if ( $redirect_to_settings_page ) {
@@ -1279,6 +1344,10 @@ public function maybe_handle_onboarding() {
// in the "everything OK" scenario).
if ( WC_Payments_Onboarding_Service::is_test_mode_enabled() ) {
try {
+ // If we're in test mode and dealing with a test-drive account,
+ // we need to collect the test drive settings before we delete the test-drive account,
+ // and apply those settings to the live account.
+ $this->save_test_drive_settings();
// Delete the currently connected Stripe account.
$this->payments_api_client->delete_account( true );
} catch ( API_Exception $e ) {
@@ -1363,7 +1432,6 @@ public function maybe_handle_onboarding() {
if ( ! $collect_payout_requirements
&& $this->has_working_jetpack_connection()
&& $this->is_stripe_account_valid() ) {
-
$params = [
'source' => $onboarding_source,
// Carry over some parameters as they may be used by our frontend logic.
@@ -1429,7 +1497,7 @@ public function maybe_handle_onboarding() {
// If there is a working one, we can proceed with the Stripe account handling.
try {
$this->maybe_init_jetpack_connection(
- // Carry over all the important GET params, so we have them after the Jetpack connection setup.
+ // Carry over all the important GET params, so we have them after the Jetpack connection setup.
add_query_arg(
[
'promo' => ! empty( $incentive_id ) ? $incentive_id : false,
@@ -1438,6 +1506,10 @@ public function maybe_handle_onboarding() {
'test_mode' => $should_onboard_in_test_mode ? 'true' : false,
'test_drive' => $create_test_drive_account ? 'true' : false,
'auto_start_test_drive_onboarding' => $auto_start_test_drive_onboarding ? 'true' : false,
+ // These are starting capabilities for the account.
+ // They are collected by the payment method step of the
+ // WC Payments settings page native onboarding experience.
+ 'capabilities' => rawurlencode( wp_json_encode( $this->onboarding_service->get_capabilities_from_request() ) ),
'from' => WC_Payments_Onboarding_Service::FROM_WPCOM_CONNECTION,
'source' => $onboarding_source,
'redirect_to_settings_page' => $redirect_to_settings_page ? 'true' : false,
@@ -1466,13 +1538,19 @@ public function maybe_handle_onboarding() {
&& WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD !== $from
&& ! $this->is_stripe_connected() ) {
+ $additional_params = [
+ 'source' => $onboarding_source,
+ ];
+
+ if ( $this->onboarding_service->get_capabilities_from_request() ) {
+ $additional_params['capabilities'] = rawurlencode( wp_json_encode( $this->onboarding_service->get_capabilities_from_request() ) );
+ }
+
$this->redirect_service->redirect_to_onboarding_wizard(
// When we redirect to the onboarding wizard, we carry over the `from`, if we have it.
// This is because there is no interim step between the user clicking the connect link and the onboarding wizard.
! empty( $from ) ? $from : $next_step_from,
- [
- 'source' => $onboarding_source,
- ]
+ $additional_params
);
return;
}
@@ -1505,11 +1583,15 @@ public function maybe_handle_onboarding() {
null,
$from, // Carry over `from` since we are doing a short-circuit.
[
- 'promo' => ! empty( $incentive_id ) ? $incentive_id : false,
- 'test_drive' => 'true',
+ 'promo' => ! empty( $incentive_id ) ? $incentive_id : false,
+ 'test_drive' => 'true',
'auto_start_test_drive_onboarding' => 'true', // This is critical.
- 'test_mode' => $should_onboard_in_test_mode ? 'true' : false,
- 'source' => $onboarding_source,
+ // These are starting capabilities for the account.
+ // They are collected by the payment method step of the
+ // WC Payments settings page native onboarding experience.
+ 'capabilities' => rawurlencode( wp_json_encode( $this->onboarding_service->get_capabilities_from_request() ) ),
+ 'test_mode' => $should_onboard_in_test_mode ? 'true' : false,
+ 'source' => $onboarding_source,
'redirect_to_settings_page' => $redirect_to_settings_page ? 'true' : false,
]
);
@@ -1914,6 +1996,7 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne
}
$self_assessment_data = isset( $_GET['self_assessment'] ) ? wc_clean( wp_unslash( $_GET['self_assessment'] ) ) : [];
+
if ( 'test_drive' === $setup_mode ) {
// If we get to the overview page, we want to show the success message.
$return_url = add_query_arg( 'wcpay-sandbox-success', 'true', $return_url );
@@ -1928,7 +2011,14 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne
];
$user_data = $this->onboarding_service->get_onboarding_user_data();
- $account_data = $this->onboarding_service->get_account_data( $setup_mode, $self_assessment_data );
+ $account_data = $this->onboarding_service->get_account_data(
+ $setup_mode,
+ $self_assessment_data,
+ // These are starting capabilities for the account.
+ // They are collected by the payment method step of the
+ // WC Payments settings page native onboarding experience.
+ $this->onboarding_service->get_capabilities_from_request()
+ );
$onboarding_data = $this->payments_api_client->get_onboarding_data(
'live' === $setup_mode,
@@ -1941,9 +2031,19 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne
$collect_payout_requirements
);
+ $should_enable_woopay = filter_var( $onboarding_data['woopay_enabled_by_default'] ?? false, FILTER_VALIDATE_BOOLEAN );
+ $is_test_mode = in_array( $setup_mode, [ 'test', 'test_drive' ], true );
+ $account_already_exists = isset( $onboarding_data['url'] ) && false === $onboarding_data['url'];
+
+ // Only store the 'woopay_enabled_by_default' flag in a transient, to be enabled later, if
+ // it should be enabled and the account doesn't already exist, or we are in test mode.
+ if ( $should_enable_woopay && ( ! $account_already_exists || $is_test_mode ) ) {
+ set_transient( self::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT, $should_enable_woopay, DAY_IN_SECONDS );
+ }
+
// If an account already exists for this site and/or there is no need for KYC verifications, we're done.
// Our platform will respond with a `false` URL in this case.
- if ( isset( $onboarding_data['url'] ) && false === $onboarding_data['url'] ) {
+ if ( $account_already_exists ) {
// Set the gateway options.
$gateway = WC_Payments::get_gateway();
$gateway->update_option( 'enabled', 'yes' );
@@ -1964,9 +2064,6 @@ private function init_stripe_onboarding( string $setup_mode, string $wcpay_conne
);
}
- // We have an account that needs to be verified (has a URL to redirect the merchant to).
- // Store the relevant onboarding data.
- set_transient( self::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT, filter_var( $onboarding_data['woopay_enabled_by_default'] ?? false, FILTER_VALIDATE_BOOLEAN ), DAY_IN_SECONDS );
// Save the onboarding state for a day.
// This is used to verify the state when finalizing the onboarding and connecting the account.
// On finalizing the onboarding, the transient gets deleted.
@@ -2086,13 +2183,11 @@ private function finalize_connection( string $state, string $mode, array $additi
// If we get this parameter, but we have a valid state, it means the merchant left KYC early and didn't finish it.
// While we do have an account, it is not yet valid. We need to redirect them back to the connect page.
$params['wcpay-connection-error'] = '1';
-
$this->redirect_service->redirect_to_connect_page( '', WC_Payments_Onboarding_Service::FROM_STRIPE, $params );
return;
}
$params['wcpay-connection-success'] = '1';
-
$this->redirect_service->redirect_to_overview_page( WC_Payments_Onboarding_Service::FROM_STRIPE, $params );
}
@@ -2519,4 +2614,44 @@ public function get_lifetime_total_payment_volume(): int {
$account = $this->get_cached_account_data();
return (int) ! empty( $account ) && isset( $account['lifetime_total_payment_volume'] ) ? $account['lifetime_total_payment_volume'] : 0;
}
+
+ /**
+ * Extract the useful test drive settings from the account data.
+ *
+ * We will use this data to migrate the test drive settings when onboarding the live account.
+ * ATM we only store the enabled payment methods.
+ *
+ * @return array The test drive settings for the live account.
+ */
+ private function get_test_drive_settings_for_live_account(): array {
+ $gateway = WC_Payments::get_gateway();
+
+ $capabilities = [];
+ foreach ( $gateway->get_upe_enabled_payment_method_ids() as $payment_method_id ) {
+ $capabilities[ $payment_method_id . '_payments' ] = [ 'requested' => 'true' ];
+ }
+
+ return [ 'capabilities' => $capabilities ];
+ }
+
+ /**
+ * If we're in test mode and dealing with a test-drive account,
+ * we need to collect the test drive settings before we delete the test-drive account,
+ * and apply those settings to the live account.
+ *
+ * @return void
+ */
+ private function save_test_drive_settings(): void {
+ $account = $this->get_cached_account_data();
+
+ if ( ! empty( $account['is_test_drive'] ) && true === $account['is_test_drive'] ) {
+ $test_drive_account_data = $this->get_test_drive_settings_for_live_account();
+
+ // Store the test drive settings for the live account in a transient,
+ // We don't passing the data around, as the merchant might cancel and start
+ // the onboarding from scratch. In this case, we won't have the test drive
+ // account anymore to collect the settings.
+ set_transient( self::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT, $test_drive_account_data, HOUR_IN_SECONDS );
+ }
+ }
}
diff --git a/includes/class-wc-payments-captured-event-note.php b/includes/class-wc-payments-captured-event-note.php
index 10c48567952..07e902d8632 100644
--- a/includes/class-wc-payments-captured-event-note.php
+++ b/includes/class-wc-payments-captured-event-note.php
@@ -327,9 +327,9 @@ private function fee_label_mapping( int $fixed_rate, bool $is_capped ) {
$res['additional-fx'] = 0 !== $fixed_rate
/* translators: %1$s% is the fee percentage and %2$s is the fixed rate */
- ? __( 'Foreign exchange fee: %1$s%% + %2$s', 'woocommerce-payments' )
+ ? __( 'Currency conversion fee: %1$s%% + %2$s', 'woocommerce-payments' )
/* translators: %1$s% is the fee percentage */
- : __( 'Foreign exchange fee: %1$s%%', 'woocommerce-payments' );
+ : __( 'Currency conversion fee: %1$s%%', 'woocommerce-payments' );
$res['additional-wcpay-subscription'] = 0 !== $fixed_rate
/* translators: %1$s% is the fee percentage and %2$s is the fixed rate */
diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php
index ee7a161f3b1..44d92d10b23 100644
--- a/includes/class-wc-payments-checkout.php
+++ b/includes/class-wc-payments-checkout.php
@@ -103,6 +103,7 @@ public function init_hooks() {
add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts_for_zero_order_total' ], 11 );
+ add_action( 'woocommerce_after_checkout_form', [ $this, 'maybe_load_checkout_scripts' ] );
}
/**
@@ -151,11 +152,18 @@ public function register_scripts_for_zero_order_total() {
! has_block( 'woocommerce/checkout' ) &&
! wp_script_is( 'wcpay-upe-checkout', 'enqueued' )
) {
- WC_Payments::get_gateway()->tokenization_script();
- $script_handle = 'wcpay-upe-checkout';
- $js_object = 'wcpay_upe_config';
- wp_localize_script( $script_handle, $js_object, WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() );
- wp_enqueue_script( $script_handle );
+ $this->load_checkout_scripts();
+ }
+ }
+
+ /**
+ * Sometimes the filters can remove the payment gateway from the checkout page which results in the payment fields not being displayed.
+ * This could prevent loading of the payment fields (checkout) scripts.
+ * This function ensures that these scripts are loaded.
+ */
+ public function maybe_load_checkout_scripts() {
+ if ( is_checkout() && ! wp_script_is( 'wcpay-upe-checkout', 'enqueued' ) ) {
+ $this->load_checkout_scripts();
}
}
@@ -416,7 +424,7 @@ function () use ( $prepared_customer_data ) {
}
?>
-
gateway = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id );
}
}
+
+ /**
+ * Load the checkout scripts.
+ */
+ private function load_checkout_scripts() {
+ WC_Payments::get_gateway()->tokenization_script();
+ $script_handle = 'wcpay-upe-checkout';
+ $js_object = 'wcpay_upe_config';
+ wp_localize_script( $script_handle, $js_object, WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() );
+ wp_enqueue_script( $script_handle );
+ }
}
diff --git a/includes/class-wc-payments-customer-service.php b/includes/class-wc-payments-customer-service.php
index 05f95c32d31..d0f97e061c0 100644
--- a/includes/class-wc-payments-customer-service.php
+++ b/includes/class-wc-payments-customer-service.php
@@ -99,7 +99,12 @@ public function __construct(
$this->database_cache = $database_cache;
$this->session_service = $session_service;
$this->order_service = $order_service;
+ }
+ /**
+ * Initialize hooks
+ */
+ public function init_hooks() {
/*
* Adds the WooCommerce Payments customer ID found in the user session
* to the WordPress user as metadata.
diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php
index 8700fa4fa29..504ac3bd5e4 100644
--- a/includes/class-wc-payments-onboarding-service.php
+++ b/includes/class-wc-payments-onboarding-service.php
@@ -109,6 +109,7 @@ public function __construct( WC_Payments_API_Client $payments_api_client, Databa
*/
public function init_hooks() {
add_filter( 'admin_body_class', [ $this, 'add_admin_body_classes' ] );
+ add_filter( 'wc_payments_get_onboarding_data_args', [ $this, 'maybe_add_test_drive_settings_to_new_account_request' ] );
}
/**
@@ -150,6 +151,98 @@ function () use ( $locale ) {
);
}
+ /**
+ * Retrieve and cache the account recommended payment methods list.
+ *
+ * @param string $country_code The account's business location country code. Provide a 2-letter ISO country code.
+ * @param string $locale Optional. The locale to use to i18n the data.
+ *
+ * @return ?array The recommended payment methods list.
+ * NULL on retrieval or validation error.
+ */
+ public function get_recommended_payment_methods( string $country_code, string $locale = '' ): ?array {
+ $cache_key = Database_Cache::RECOMMENDED_PAYMENT_METHODS . '__' . $country_code;
+ if ( ! empty( $locale ) ) {
+ $cache_key .= '__' . $locale;
+ }
+
+ return \WC_Payments::get_database_cache()->get_or_add(
+ $cache_key,
+ function () use ( $country_code, $locale ) {
+ try {
+ return $this->payments_api_client->get_recommended_payment_methods( $country_code, $locale );
+ } catch ( API_Exception $e ) {
+ // Return NULL to signal retrieval error.
+ return null;
+ }
+ },
+ 'is_array'
+ );
+ }
+
+ /**
+ * Get the onboarding capabilities from the request.
+ *
+ * The capabilities are expected to be passed as an array of capabilities keyed by the capability ID and
+ * with boolean values. If the value is true, the capability is requested when the account is created.
+ *
+ * @return array The standardized capabilities that were passed in the request.
+ * Empty array if no capabilities were passed or none were valid.
+ */
+ public function get_capabilities_from_request(): array {
+ $capabilities = [];
+
+ if ( empty( $_REQUEST['capabilities'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended
+ return $capabilities;
+ }
+
+ // Try to extract the capabilities.
+ // They might be already decoded or not, so we need to handle both cases.
+ // We expect them to be an array.
+ // We disable the warning because we have our own sanitization and validation.
+ // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ $capabilities = wp_unslash( $_REQUEST['capabilities'] );
+ if ( ! is_array( $capabilities ) ) {
+ $capabilities = json_decode( $capabilities, true ) ?? [];
+ }
+
+ if ( empty( $capabilities ) ) {
+ return [];
+ }
+
+ // Sanitize and validate.
+ $capabilities = array_combine(
+ array_map(
+ function ( $key ) {
+ // Keep numeric keys as integers so we can remove them later.
+ if ( is_numeric( $key ) ) {
+ return intval( $key );
+ }
+
+ return sanitize_text_field( $key );
+ },
+ array_keys( $capabilities )
+ ),
+ array_map(
+ function ( $value ) {
+ return filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
+ },
+ $capabilities
+ )
+ );
+
+ // Filter out any invalid entries.
+ $capabilities = array_filter(
+ $capabilities,
+ function ( $value, $key ) {
+ return is_string( $key ) && is_bool( $value );
+ },
+ ARRAY_FILTER_USE_BOTH
+ );
+
+ return $capabilities;
+ }
+
/**
* Retrieve the embedded KYC session and handle initial account creation (if necessary).
*
@@ -177,15 +270,19 @@ public function create_embedded_kyc_session( array $self_assessment_data, bool $
'site_locale' => get_locale(),
];
$user_data = $this->get_onboarding_user_data();
- $account_data = $this->get_account_data( $setup_mode, $self_assessment_data );
+ $account_data = $this->get_account_data(
+ $setup_mode,
+ $self_assessment_data,
+ $this->get_capabilities_from_request()
+ );
$actioned_notes = self::get_actioned_notes();
try {
$account_session = $this->payments_api_client->initialize_onboarding_embedded_kyc(
'live' === $setup_mode,
$site_data,
- array_filter( $user_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped.
- array_filter( $account_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped.
+ WC_Payments_Utils::array_filter_recursive( $user_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped.
+ WC_Payments_Utils::array_filter_recursive( $account_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped.
$actioned_notes,
$progressive
);
@@ -335,12 +432,15 @@ public function add_admin_body_classes( string $classes = '' ): string {
/**
* Get account data for onboarding from self assessment data.
*
- * @param string $setup_mode Setup mode.
+ * @param string $setup_mode Setup mode.
* @param array $self_assessment_data Self assessment data.
+ * @param array $capabilities Optional. List keyed by capabilities IDs (payment methods) with boolean values.
+ * If the value is true, the capability is requested when the account is created.
+ * If the value is false, the capability is not requested when the account is created.
*
* @return array Account data.
*/
- public function get_account_data( string $setup_mode, array $self_assessment_data ): array {
+ public function get_account_data( string $setup_mode, array $self_assessment_data, array $capabilities = [] ): array {
$home_url = get_home_url();
// If the site is running on localhost, use a bogus URL. This is to avoid Stripe's errors.
// wp_http_validate_url does not check that, unfortunately.
@@ -357,6 +457,33 @@ public function get_account_data( string $setup_mode, array $self_assessment_dat
'business_name' => get_bloginfo( 'name' ),
];
+ foreach ( $capabilities as $capability => $should_request ) {
+ // Remove the `_payments` suffix from the capability, if present.
+ if ( strpos( $capability, '_payments' ) === strlen( $capability ) - 9 ) {
+ $capability = str_replace( '_payments', '', $capability );
+ }
+
+ // Skip the special 'apple_google' because it is not a payment method.
+ // Skip the 'woopay' because it is automatically handled by the API.
+ if ( 'apple_google' === $capability || 'woopay' === $capability ) {
+ continue;
+ }
+
+ if ( 'card' === $capability ) {
+ // Card is always requested.
+ $account_data['capabilities']['card_payments'] = [ 'requested' => 'true' ];
+ // When requesting card, we also need to request transfers.
+ // The platform should handle this automatically, but it is best to be thorough.
+ $account_data['capabilities']['transfers'] = [ 'requested' => 'true' ];
+ continue;
+ }
+
+ // We only request, not unrequest capabilities.
+ if ( $should_request ) {
+ $account_data['capabilities'][ $capability . '_payments' ] = [ 'requested' => 'true' ];
+ }
+ }
+
if ( ! empty( $self_assessment_data ) ) {
$business_type = $self_assessment_data['business_type'] ?? null;
$account_data = WC_Payments_Utils::array_merge_recursive_distinct(
@@ -406,6 +533,7 @@ public function get_account_data( string $setup_mode, array $self_assessment_dat
]
);
}
+
return $account_data;
}
@@ -873,4 +1001,27 @@ public static function get_source( ?string $referer = null, ?array $get_params =
// Default to an unknown source.
return self::SOURCE_UNKNOWN;
}
+
+ /**
+ * If settings are collected from the test-drive account,
+ * include them in the existing arguments when creating the new account.
+ *
+ * @param array $args The request args to create new account.
+ *
+ * @return array The request args, possible updated with the test drive account settings, used to create new account.
+ */
+ public function maybe_add_test_drive_settings_to_new_account_request( array $args ): array {
+ if (
+ get_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ) &&
+ is_array( get_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ) )
+ ) {
+ $args['account_data'] = array_merge(
+ $args['account_data'],
+ get_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT )
+ );
+ delete_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT );
+ }
+
+ return $args;
+ }
}
diff --git a/includes/class-wc-payments-order-service.php b/includes/class-wc-payments-order-service.php
index c563877e830..f685b50debf 100644
--- a/includes/class-wc-payments-order-service.php
+++ b/includes/class-wc-payments-order-service.php
@@ -190,6 +190,21 @@ public function update_order_status_from_intent( $order, $intent ) {
$this->complete_order_processing( $order );
}
+ /**
+ * Handles the order state when a payment is captured successfully.
+ * Unlike `update_order_status_from_intent`, this method does not check the current order status or skip processing
+ * if the order is already in the "processing" state. This ensures the order status is updated correctly upon a
+ * successful capture, preventing issues where the capture is not reflected in the order details or transaction screens
+ * due to the order status being in the processing state.
+ *
+ * @param WC_Order $order The order to update.
+ * @param WC_Payments_API_Abstract_Intention $intent The intent object containing payment or setup data.
+ */
+ public function process_captured_payment( $order, $intent ) {
+ $this->mark_payment_capture_completed( $order, $intent );
+ $this->complete_order_processing( $order, $intent->get_status() );
+ }
+
/**
* Updates an order to failed status, while adding a note with a link to the transaction.
*
diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php
deleted file mode 100644
index 82b33593008..00000000000
--- a/includes/class-wc-payments-payment-request-button-handler.php
+++ /dev/null
@@ -1,873 +0,0 @@
-account = $account;
- $this->gateway = $gateway;
- $this->express_checkout_helper = $express_checkout_helper;
- }
-
- /**
- * Initialize hooks.
- *
- * @return void
- */
- public function init() {
- // Checks if WCPay is enabled.
- if ( ! $this->gateway->is_enabled() ) {
- return;
- }
-
- if ( ! WC_Payments_Features::is_tokenized_cart_ece_enabled() ) {
- return;
- }
-
- // Checks if Payment Request is enabled.
- if ( 'yes' !== $this->gateway->get_option( 'payment_request' ) ) {
- return;
- }
-
- // Don't load for change payment method page.
- if ( isset( $_GET['change_payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
- return;
- }
-
- add_action( 'template_redirect', [ $this, 'set_session' ] );
- add_action( 'template_redirect', [ $this, 'handle_payment_request_redirect' ] );
- add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] );
-
- add_filter( 'woocommerce_gateway_title', [ $this, 'filter_gateway_title' ], 10, 2 );
- add_action( 'woocommerce_checkout_order_processed', [ $this, 'add_order_meta' ], 10, 2 );
- add_filter( 'woocommerce_login_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 );
- add_filter( 'woocommerce_registration_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 );
- add_filter( 'woocommerce_cart_needs_shipping_address', [ $this, 'filter_cart_needs_shipping_address' ], 11, 1 );
-
- // Add a filter for the value of `wcpay_is_apple_pay_enabled`.
- // This option does not get stored in the database at all, and this function
- // will be used to calculate it whenever the option value is retrieved instead.
- // It's used for displaying inbox notifications.
- add_filter( 'pre_option_wcpay_is_apple_pay_enabled', [ $this, 'get_option_is_apple_pay_enabled' ], 10, 1 );
- }
-
- /**
- * Checks whether authentication is required for checkout.
- *
- * @return bool
- */
- public function is_authentication_required() {
- // If guest checkout is disabled and account creation is not possible, authentication is required.
- if ( 'no' === get_option( 'woocommerce_enable_guest_checkout', 'yes' ) && ! $this->is_account_creation_possible() ) {
- return true;
- }
- // If cart contains subscription and account creation is not posible, authentication is required.
- if ( $this->has_subscription_product() && ! $this->is_account_creation_possible() ) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Checks whether account creation is possible during checkout.
- *
- * @return bool
- */
- public function is_account_creation_possible() {
- $is_signup_from_checkout_allowed = 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout', 'no' );
-
- // If a subscription is being purchased, check if account creation is allowed for subscriptions.
- if ( ! $is_signup_from_checkout_allowed && $this->has_subscription_product() ) {
- $is_signup_from_checkout_allowed = 'yes' === get_option( 'woocommerce_enable_signup_from_checkout_for_subscriptions', 'no' );
- }
-
- // If automatically generate username/password are disabled, the Payment Request API
- // can't include any of those fields, so account creation is not possible.
- return (
- $is_signup_from_checkout_allowed &&
- 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) &&
- 'yes' === get_option( 'woocommerce_registration_generate_password', 'yes' )
- );
- }
-
- /**
- * Sets the WC customer session if one is not set.
- * This is needed so nonces can be verified by AJAX Request.
- *
- * @return void
- */
- public function set_session() {
- // Don't set session cookies on product pages to allow for caching when payment request
- // buttons are disabled. But keep cookies if there is already an active WC session in place.
- if (
- ! ( $this->express_checkout_helper->is_product() && $this->should_show_payment_request_button() )
- || ( isset( WC()->session ) && WC()->session->has_session() )
- ) {
- return;
- }
-
- WC()->session->set_customer_session_cookie( true );
- }
-
- /**
- * Handles payment request redirect when the redirect dialog "Continue" button is clicked.
- */
- public function handle_payment_request_redirect() {
- if (
- ! empty( $_GET['wcpay_payment_request_redirect_url'] )
- && ! empty( $_GET['_wpnonce'] )
- && wp_verify_nonce( $_GET['_wpnonce'], 'wcpay-set-redirect-url' ) // @codingStandardsIgnoreLine
- ) {
- $url = rawurldecode( esc_url_raw( wp_unslash( $_GET['wcpay_payment_request_redirect_url'] ) ) );
- // Sets a redirect URL cookie for 10 minutes, which we will redirect to after authentication.
- // Users will have a 10 minute timeout to login/create account, otherwise redirect URL expires.
- wc_setcookie( 'wcpay_payment_request_redirect_url', $url, time() + MINUTE_IN_SECONDS * 10 );
- // Redirects to "my-account" page.
- wp_safe_redirect( get_permalink( get_option( 'woocommerce_myaccount_page_id' ) ) );
- }
- }
-
- /**
- * The settings for the `button` attribute - they depend on the "grouped settings" flag value.
- *
- * @return array
- */
- public function get_button_settings() {
- $button_type = $this->gateway->get_option( 'payment_request_button_type' );
- $common_settings = $this->express_checkout_helper->get_common_button_settings();
- $payment_request_button_settings = [
- // Default format is en_US.
- 'locale' => apply_filters( 'wcpay_payment_request_button_locale', substr( get_locale(), 0, 2 ) ),
- 'branded_type' => 'default' === $button_type ? 'short' : 'long',
- ];
-
- return array_merge( $common_settings, $payment_request_button_settings );
- }
-
- /**
- * Gets the product total price.
- *
- * @param object $product WC_Product_* object.
- * @param bool $is_deposit Whether customer is paying a deposit.
- * @param int $deposit_plan_id The ID of the deposit plan.
- *
- * @return mixed Total price.
- *
- * @throws Invalid_Price_Exception Whenever a product has no price.
- */
- public function get_product_price( $product, ?bool $is_deposit = null, int $deposit_plan_id = 0 ) {
- // If prices should include tax, using tax inclusive price.
- if ( $this->express_checkout_helper->cart_prices_include_tax() ) {
- $base_price = wc_get_price_including_tax( $product );
- } else {
- $base_price = wc_get_price_excluding_tax( $product );
- }
-
- // If WooCommerce Deposits is active, we need to get the correct price for the product.
- if ( class_exists( 'WC_Deposits_Product_Manager' ) && class_exists( 'WC_Deposits_Plans_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) {
- // If is_deposit is null, we use the default deposit type for the product.
- if ( is_null( $is_deposit ) ) {
- $is_deposit = 'deposit' === WC_Deposits_Product_Manager::get_deposit_selected_type( $product->get_id() );
- }
- if ( $is_deposit ) {
- $deposit_type = WC_Deposits_Product_Manager::get_deposit_type( $product->get_id() );
- $available_plan_ids = WC_Deposits_Plans_Manager::get_plan_ids_for_product( $product->get_id() );
- // Default to first (default) plan if no plan is specified.
- if ( 'plan' === $deposit_type && 0 === $deposit_plan_id && ! empty( $available_plan_ids ) ) {
- $deposit_plan_id = $available_plan_ids[0];
- }
-
- // Ensure the selected plan is available for the product.
- if ( 0 === $deposit_plan_id || in_array( $deposit_plan_id, $available_plan_ids, true ) ) {
- $base_price = WC_Deposits_Product_Manager::get_deposit_amount( $product, $deposit_plan_id, 'display', $base_price );
- }
- }
- }
-
- // Add subscription sign-up fees to product price.
- $sign_up_fee = 0;
- $subscription_types = [
- 'subscription',
- 'subscription_variation',
- ];
- if ( in_array( $product->get_type(), $subscription_types, true ) && class_exists( 'WC_Subscriptions_Product' ) ) {
- // When there is no sign-up fee, `get_sign_up_fee` falls back to an int 0.
- $sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $product );
- }
-
- if ( ! is_numeric( $base_price ) || ! is_numeric( $sign_up_fee ) ) {
- $error_message = sprintf(
- // Translators: %d is the numeric ID of the product without a price.
- __( 'Express checkout does not support products without prices! Please add a price to product #%d', 'woocommerce-payments' ),
- (int) $product->get_id()
- );
- throw new Invalid_Price_Exception(
- esc_html( $error_message )
- );
- }
-
- return $base_price + $sign_up_fee;
- }
-
- /**
- * Gets the product data for the currently viewed page.
- *
- * @return mixed Returns false if not on a product page, the product information otherwise.
- */
- public function get_product_data() {
- if ( ! $this->express_checkout_helper->is_product() ) {
- return false;
- }
-
- /** @var WC_Product_Variable $product */ // phpcs:ignore
- $product = $this->express_checkout_helper->get_product();
- $currency = get_woocommerce_currency();
-
- if ( 'variable' === $product->get_type() || 'variable-subscription' === $product->get_type() ) {
- $variation_attributes = $product->get_variation_attributes();
- $attributes = [];
-
- foreach ( $variation_attributes as $attribute_name => $attribute_values ) {
- $attribute_key = 'attribute_' . sanitize_title( $attribute_name );
-
- // Passed value via GET takes precedence. Otherwise get the default value for given attribute.
- $attributes[ $attribute_key ] = isset( $_GET[ $attribute_key ] ) // phpcs:ignore WordPress.Security.NonceVerification
- ? wc_clean( wp_unslash( $_GET[ $attribute_key ] ) ) // phpcs:ignore WordPress.Security.NonceVerification
- : $product->get_variation_default_attribute( $attribute_name );
- }
-
- $data_store = WC_Data_Store::load( 'product' );
- $variation_id = $data_store->find_matching_product_variation( $product, $attributes );
-
- if ( ! empty( $variation_id ) ) {
- $product = wc_get_product( $variation_id );
- }
- }
-
- try {
- $price = $this->get_product_price( $product );
- } catch ( Invalid_Price_Exception $e ) {
- Logger::log( $e->getMessage() );
-
- return false;
- }
-
- $data = [];
- $items = [];
-
- $items[] = [
- 'label' => $product->get_name(),
- 'amount' => WC_Payments_Utils::prepare_amount( $price, $currency ),
- ];
-
- $total_tax = 0;
- foreach ( $this->get_taxes_like_cart( $product, $price ) as $tax ) {
- $total_tax += $tax;
-
- $items[] = [
- 'label' => __( 'Tax', 'woocommerce-payments' ),
- 'amount' => WC_Payments_Utils::prepare_amount( $tax, $currency ),
- 'pending' => 0 === $tax,
- ];
- }
-
- if ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() ) {
- $items[] = [
- 'label' => __( 'Shipping', 'woocommerce-payments' ),
- 'amount' => 0,
- 'pending' => true,
- ];
-
- $data['shippingOptions'] = [
- 'id' => 'pending',
- 'label' => __( 'Pending', 'woocommerce-payments' ),
- 'detail' => '',
- 'amount' => 0,
- ];
- }
-
- $data['displayItems'] = $items;
- $data['total'] = [
- 'label' => apply_filters( 'wcpay_payment_request_total_label', $this->express_checkout_helper->get_total_label() ),
- 'amount' => WC_Payments_Utils::prepare_amount( $price + $total_tax, $currency ),
- 'pending' => true,
- ];
-
- $data['needs_shipping'] = ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() );
- $data['currency'] = strtolower( $currency );
- $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 );
-
- return apply_filters( 'wcpay_payment_request_product_data', $data, $product );
- }
-
- /**
- * Filters the gateway title to reflect Payment Request type
- *
- * @param string $title Gateway title.
- * @param string $id Gateway ID.
- */
- public function filter_gateway_title( $title, $id ) {
- if ( 'woocommerce_payments' !== $id || ! is_admin() ) {
- return $title;
- }
-
- $order = $this->get_current_order();
- $method_title = is_object( $order ) ? $order->get_payment_method_title() : '';
-
- if ( ! empty( $method_title ) ) {
- if (
- strpos( $method_title, 'Apple Pay' ) === 0
- || strpos( $method_title, 'Google Pay' ) === 0
- || strpos( $method_title, 'Payment Request' ) === 0
- ) {
- return $method_title;
- }
- }
-
- return $title;
- }
-
- /**
- * Used to get the order in admin edit page.
- *
- * @return WC_Order|WC_Order_Refund|bool
- */
- private function get_current_order() {
- global $theorder;
- global $post;
-
- if ( is_object( $theorder ) ) {
- return $theorder;
- }
-
- if ( is_object( $post ) ) {
- return wc_get_order( $post->ID );
- }
-
- return false;
- }
-
- /**
- * Normalizes postal code in case of redacted data from Apple Pay.
- *
- * @param string $postcode Postal code.
- * @param string $country Country.
- */
- public function get_normalized_postal_code( $postcode, $country ) {
- /**
- * Currently, Apple Pay truncates the UK and Canadian postal codes to the first 4 and 3 characters respectively
- * when passing it back from the shippingcontactselected object. This causes WC to invalidate
- * the postal code and not calculate shipping zones correctly.
- */
- if ( Country_Code::UNITED_KINGDOM === $country ) {
- // Replaces a redacted string with something like N1C0000.
- return str_pad( preg_replace( '/\s+/', '', $postcode ), 7, '0' );
- }
- if ( Country_Code::CANADA === $country ) {
- // Replaces a redacted string with something like H3B000.
- return str_pad( preg_replace( '/\s+/', '', $postcode ), 6, '0' );
- }
-
- return $postcode;
- }
-
- /**
- * Add needed order meta
- *
- * @param integer $order_id The order ID.
- *
- * @return void
- */
- public function add_order_meta( $order_id ) {
- if ( empty( $_POST['payment_request_type'] ) || ! isset( $_POST['payment_method'] ) || 'woocommerce_payments' !== $_POST['payment_method'] ) { // phpcs:ignore WordPress.Security.NonceVerification
- return;
- }
-
- $order = wc_get_order( $order_id );
-
- $payment_request_type = wc_clean( wp_unslash( $_POST['payment_request_type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
-
- $payment_method_titles = [
- 'apple_pay' => 'Apple Pay',
- 'google_pay' => 'Google Pay',
- ];
-
- $suffix = apply_filters( 'wcpay_payment_request_payment_method_title_suffix', 'WooPayments' );
- if ( ! empty( $suffix ) ) {
- $suffix = " ($suffix)";
- }
-
- $payment_method_title = isset( $payment_method_titles[ $payment_request_type ] ) ? $payment_method_titles[ $payment_request_type ] : 'Payment Request';
- $order->set_payment_method_title( $payment_method_title . $suffix );
- $order->save();
- }
-
- /**
- * Checks whether Payment Request Button should be available on this page.
- *
- * @return bool
- */
- public function should_show_payment_request_button() {
- // If account is not connected, then bail.
- if ( ! $this->account->is_stripe_connected() ) {
- return false;
- }
-
- // If no SSL, bail.
- if ( ! WC_Payments::mode()->is_test() && ! is_ssl() ) {
- Logger::log( 'Stripe Payment Request live mode requires SSL.' );
-
- return false;
- }
-
- // Page not supported.
- if ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_checkout() ) {
- return false;
- }
-
- // Product page, but not available in settings.
- if ( $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_available_at( 'product', self::BUTTON_LOCATIONS ) ) {
- return false;
- }
-
- // Checkout page, but not available in settings.
- if ( $this->express_checkout_helper->is_checkout() && ! $this->express_checkout_helper->is_available_at( 'checkout', self::BUTTON_LOCATIONS ) ) {
- return false;
- }
-
- // Cart page, but not available in settings.
- if ( $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_available_at( 'cart', self::BUTTON_LOCATIONS ) ) {
- return false;
- }
-
- // Product page, but has unsupported product type.
- if ( $this->express_checkout_helper->is_product() && ! apply_filters( 'wcpay_payment_request_is_product_supported', $this->is_product_supported(), $this->express_checkout_helper->get_product() ) ) {
- Logger::log( 'Product page has unsupported product type ( Payment Request button disabled )' );
-
- return false;
- }
-
- // Cart has unsupported product type.
- if ( ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) && ! $this->has_allowed_items_in_cart() ) {
- Logger::log( 'Items in the cart have unsupported product type ( Payment Request button disabled )' );
-
- return false;
- }
-
- // Order total doesn't matter for Pay for Order page. Thus, this page should always display payment buttons.
- if ( $this->express_checkout_helper->is_pay_for_order_page() ) {
- return true;
- }
-
- // Cart total is 0 or is on product page and product price is 0.
- // Exclude pay-for-order pages from this check.
- if (
- ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_pay_for_order_page() && 0.0 === (float) WC()->cart->get_total( 'edit' ) ) ||
- ( $this->express_checkout_helper->is_product() && 0.0 === (float) $this->express_checkout_helper->get_product()->get_price() )
-
- ) {
- Logger::log( 'Order price is 0 ( Payment Request button disabled )' );
-
- return false;
- }
-
- return true;
- }
-
- /**
- * Checks to make sure product type is supported.
- *
- * @return array
- */
- public function supported_product_types() {
- return apply_filters(
- 'wcpay_payment_request_supported_types',
- [
- 'simple',
- 'variable',
- 'variation',
- 'subscription',
- 'variable-subscription',
- 'subscription_variation',
- 'booking',
- 'bundle',
- 'composite',
- 'mix-and-match',
- ]
- );
- }
-
- /**
- * Checks the cart to see if all items are allowed to be used.
- *
- * @return boolean
- */
- public function has_allowed_items_in_cart() {
- // Pre Orders compatbility where we don't support charge upon release.
- if ( class_exists( 'WC_Pre_Orders_Cart' ) && WC_Pre_Orders_Cart::cart_contains_pre_order() && class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() ) ) {
- return false;
- }
-
- foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
- $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
-
- if ( ! in_array( $_product->get_type(), $this->supported_product_types(), true ) ) {
- return false;
- }
-
- /**
- * Filter whether product supports Payment Request Button on cart page.
- *
- * @param boolean $is_supported Whether product supports Payment Request Button on cart page.
- * @param object $_product Product object.
- *
- * @since 6.9.0
- */
- if ( ! apply_filters( 'wcpay_payment_request_is_cart_supported', true, $_product ) ) {
- return false;
- }
-
- // Trial subscriptions with shipping are not supported.
- if ( class_exists( 'WC_Subscriptions_Product' ) && WC_Subscriptions_Product::is_subscription( $_product ) && $_product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $_product ) > 0 ) {
- return false;
- }
- }
-
- // We don't support multiple packages with Payment Request Buttons because we can't offer a good UX.
- $packages = WC()->cart->get_shipping_packages();
- if ( 1 < ( is_countable( $packages ) ? count( $packages ) : 0 ) ) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Checks whether cart contains a subscription product or this is a subscription product page.
- *
- * @return boolean
- */
- public function has_subscription_product() {
- if ( ! class_exists( 'WC_Subscriptions_Product' ) ) {
- return false;
- }
-
- if ( $this->express_checkout_helper->is_product() ) {
- $product = $this->express_checkout_helper->get_product();
- if ( WC_Subscriptions_Product::is_subscription( $product ) ) {
- return true;
- }
- }
-
- if ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) {
- if ( WC_Subscriptions_Cart::cart_contains_subscription() ) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Returns the login redirect URL.
- *
- * @param string $redirect Default redirect URL.
- *
- * @return string Redirect URL.
- */
- public function get_login_redirect_url( $redirect ) {
- $url = esc_url_raw( wp_unslash( $_COOKIE['wcpay_payment_request_redirect_url'] ?? '' ) );
-
- if ( empty( $url ) ) {
- return $redirect;
- }
- wc_setcookie( 'wcpay_payment_request_redirect_url', '' );
-
- return $url;
- }
-
- /**
- * Load public scripts and styles.
- */
- public function scripts() {
- // Don't load scripts if page is not supported.
- if ( ! $this->should_show_payment_request_button() ) {
- return;
- }
-
- $payment_request_params = [
- 'ajax_url' => admin_url( 'admin-ajax.php' ),
- 'stripe' => [
- 'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ),
- 'accountId' => $this->account->get_stripe_account_id(),
- 'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ),
- ],
- 'nonce' => [
- 'get_cart_details' => wp_create_nonce( 'wcpay-get-cart-details' ),
- 'shipping' => wp_create_nonce( 'wcpay-payment-request-shipping' ),
- 'update_shipping' => wp_create_nonce( 'wcpay-update-shipping-method' ),
- 'checkout' => wp_create_nonce( 'woocommerce-process_checkout' ),
- 'add_to_cart' => wp_create_nonce( 'wcpay-add-to-cart' ),
- 'empty_cart' => wp_create_nonce( 'wcpay-empty-cart' ),
- 'get_selected_product_data' => wp_create_nonce( 'wcpay-get-selected-product-data' ),
- 'platform_tracker' => wp_create_nonce( 'platform_tracks_nonce' ),
- 'pay_for_order' => wp_create_nonce( 'pay_for_order' ),
- 'tokenized_cart_nonce' => wp_create_nonce( 'woopayments_tokenized_cart_nonce' ),
- 'tokenized_cart_session_nonce' => wp_create_nonce( 'woopayments_tokenized_cart_session_nonce' ),
- 'store_api_nonce' => wp_create_nonce( 'wc_store_api' ),
- ],
- 'checkout' => [
- 'currency_code' => strtolower( get_woocommerce_currency() ),
- 'currency_decimals' => WC_Payments::get_localization_service()->get_currency_format( get_woocommerce_currency() )['num_decimals'],
- 'country_code' => substr( get_option( 'woocommerce_default_country' ), 0, 2 ),
- 'needs_shipping' => WC()->cart->needs_shipping(),
- // Defaults to 'required' to match how core initializes this option.
- 'needs_payer_phone' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ),
- ],
- 'button' => $this->get_button_settings(),
- 'login_confirmation' => $this->get_login_confirmation_settings(),
- 'has_block' => has_block( 'woocommerce/cart' ) || has_block( 'woocommerce/checkout' ),
- 'product' => $this->get_product_data(),
- 'total_label' => $this->express_checkout_helper->get_total_label(),
- 'button_context' => $this->express_checkout_helper->get_button_context(),
- 'is_product_page' => $this->express_checkout_helper->is_product(),
- 'is_pay_for_order' => $this->express_checkout_helper->is_pay_for_order_page(),
- 'is_checkout_page' => $this->express_checkout_helper->is_checkout(),
- ];
-
- if ( WC_Payments_Features::is_tokenized_cart_ece_enabled() ) {
- WC_Payments::register_script_with_dependencies(
- 'WCPAY_PAYMENT_REQUEST',
- 'dist/tokenized-payment-request',
- [
- 'jquery',
- 'stripe',
- ]
- );
- WC_Payments_Utils::enqueue_style(
- 'WCPAY_PAYMENT_REQUEST',
- plugins_url( 'dist/tokenized-payment-request.css', WCPAY_PLUGIN_FILE ),
- [],
- WC_Payments::get_file_version( 'dist/tokenized-payment-request.css' )
- );
- }
-
- wp_localize_script( 'WCPAY_PAYMENT_REQUEST', 'wcpayPaymentRequestParams', $payment_request_params );
-
- wp_set_script_translations( 'WCPAY_PAYMENT_REQUEST', 'woocommerce-payments' );
-
- wp_enqueue_script( 'WCPAY_PAYMENT_REQUEST' );
-
- Fraud_Prevention_Service::maybe_append_fraud_prevention_token();
-
- $gateways = WC()->payment_gateways->get_available_payment_gateways();
- if ( isset( $gateways['woocommerce_payments'] ) ) {
- WC_Payments::get_wc_payments_checkout()->register_scripts();
- }
- }
-
- /**
- * Display the payment request button.
- */
- public function display_payment_request_button_html() {
- if ( ! $this->should_show_payment_request_button() ) {
- return;
- }
- ?>
-
-
-
- express_checkout_helper->get_product();
- if ( is_null( $product ) ) {
- return false;
- }
-
- if ( ! is_object( $product ) ) {
- return false;
- }
-
- if ( ! in_array( $product->get_type(), $this->supported_product_types(), true ) ) {
- return false;
- }
-
- // Trial subscriptions with shipping are not supported.
- if ( class_exists( 'WC_Subscriptions_Product' ) && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) {
- return false;
- }
-
- // Pre Orders charge upon release not supported.
- if ( class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) {
- return false;
- }
-
- // Composite products are not supported on the product page.
- if ( class_exists( 'WC_Composite_Products' ) && $product->is_type( 'composite' ) ) {
- return false;
- }
-
- // Mix and match products are not supported on the product page.
- if ( class_exists( 'WC_Mix_and_Match' ) && $product->is_type( 'mix-and-match' ) ) {
- return false;
- }
-
- if ( class_exists( 'WC_Product_Addons_Helper' ) ) {
- // File upload addon not supported.
- $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() );
- foreach ( $product_addons as $addon ) {
- if ( 'file_upload' === $addon['type'] ) {
- return false;
- }
- }
- }
-
- return true;
- }
-
- /**
- * Determine wether to filter the cart needs shipping address.
- *
- * @param boolean $needs_shipping_address Whether the cart needs a shipping address.
- */
- public function filter_cart_needs_shipping_address( $needs_shipping_address ) {
- if ( $this->has_subscription_product() && wc_get_shipping_method_count( true, true ) === 0 ) {
- return false;
- }
-
- return $needs_shipping_address;
- }
-
- /**
- * Calculates whether Apple Pay is enabled for this store.
- * The option value is not stored in the database, and is calculated
- * using this function instead, and the values is returned by using the pre_option filter.
- *
- * The option value is retrieved for inbox notifications.
- *
- * @param mixed $value The value of the option.
- */
- public function get_option_is_apple_pay_enabled( $value ) {
- // Return a random value (1 or 2) if the account is live and payment request buttons are enabled.
- if (
- $this->gateway->is_enabled()
- && 'yes' === $this->gateway->get_option( 'payment_request' )
- && ! WC_Payments::mode()->is_dev()
- && $this->account->get_is_live()
- ) {
- $value = wp_rand( 1, 2 );
- }
-
- return $value;
- }
-
- /**
- * Settings array for the user authentication dialog and redirection.
- *
- * @return array|false
- */
- public function get_login_confirmation_settings() {
- if ( is_user_logged_in() || ! $this->is_authentication_required() ) {
- return false;
- }
-
- /* translators: The text encapsulated in `**` can be replaced with "Apple Pay" or "Google Pay". Please translate this text, but don't remove the `**`. */
- $message = __( 'To complete your transaction with **the selected payment method**, you must log in or create an account with our site.', 'woocommerce-payments' );
- $redirect_url = add_query_arg(
- [
- '_wpnonce' => wp_create_nonce( 'wcpay-set-redirect-url' ),
- 'wcpay_payment_request_redirect_url' => rawurlencode( home_url( add_query_arg( [] ) ) ),
- // Current URL to redirect to after login.
- ],
- home_url()
- );
-
- return [ // nosemgrep: audit.php.wp.security.xss.query-arg -- home_url passed in to add_query_arg.
- 'message' => $message,
- 'redirect_url' => $redirect_url,
- ];
- }
-
- /**
- * Calculates taxes as displayed on cart, based on a product and a particular price.
- *
- * @param WC_Product $product The product, for retrieval of tax classes.
- * @param float $price The price, which to calculate taxes for.
- *
- * @return array An array of final taxes.
- */
- private function get_taxes_like_cart( $product, $price ) {
- if ( ! wc_tax_enabled() || $this->express_checkout_helper->cart_prices_include_tax() ) {
- // Only proceed when taxes are enabled, but not included.
- return [];
- }
-
- // Follows the way `WC_Cart_Totals::get_item_tax_rates()` works.
- $tax_class = $product->get_tax_class();
- $rates = WC_Tax::get_rates( $tax_class );
- // No cart item, `woocommerce_cart_totals_get_item_tax_rates` can't be applied here.
-
- // Normally there should be a single tax, but `calc_tax` returns an array, let's use it.
- return WC_Tax::calc_tax( $price, $rates, false );
- }
-}
diff --git a/includes/class-wc-payments-tasks.php b/includes/class-wc-payments-tasks.php
index ee3feacff48..b0b01e22896 100644
--- a/includes/class-wc-payments-tasks.php
+++ b/includes/class-wc-payments-tasks.php
@@ -21,7 +21,11 @@ class WC_Payments_Tasks {
* WC_Payments_Admin_Tasks constructor.
*/
public static function init() {
- include_once WCPAY_ABSPATH . 'includes/admin/tasks/class-wc-payments-task-disputes.php';
+ // As WooCommerce Onboarding tasks need to hook into 'init' and requires an API call.
+ // We only add this task for users who can manage_woocommerce / view the task.
+ if ( ! current_user_can( 'manage_woocommerce' ) ) {
+ return;
+ }
add_action( 'init', [ __CLASS__, 'add_task_disputes_need_response' ] );
}
@@ -31,9 +35,11 @@ public static function init() {
*/
public static function add_task_disputes_need_response() {
$account_service = WC_Payments::get_account_service();
- if ( ! $account_service || ! $account_service->is_stripe_account_valid() ) {
+ // The task is not required if the account is not connected, under review, or rejected.
+ if ( ! $account_service || ! $account_service->is_stripe_account_valid() || $account_service->is_account_under_review() || $account_service->is_account_rejected() ) {
return;
}
+ include_once WCPAY_ABSPATH . 'includes/admin/tasks/class-wc-payments-task-disputes.php';
// 'extended' = 'Things to do next' task list on WooCommerce > Home.
TaskLists::add_task( 'extended', new WC_Payments_Task_Disputes() );
diff --git a/includes/class-wc-payments-token-service.php b/includes/class-wc-payments-token-service.php
index 7bfdc482e18..283a0d7851a 100644
--- a/includes/class-wc-payments-token-service.php
+++ b/includes/class-wc-payments-token-service.php
@@ -47,7 +47,12 @@ class WC_Payments_Token_Service {
public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Customer_Service $customer_service ) {
$this->payments_api_client = $payments_api_client;
$this->customer_service = $customer_service;
+ }
+ /**
+ * Initializes hooks.
+ */
+ public function init_hooks() {
add_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 );
add_action( 'woocommerce_payment_token_set_default', [ $this, 'woocommerce_payment_token_set_default' ], 10, 2 );
add_filter( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 );
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index eb8449bd239..b7c49bcbdec 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -354,6 +354,7 @@ public static function init() {
include_once __DIR__ . '/exceptions/class-base-exception.php';
include_once __DIR__ . '/exceptions/class-api-exception.php';
+ include_once __DIR__ . '/exceptions/class-api-merchant-exception.php';
include_once __DIR__ . '/exceptions/class-connection-exception.php';
include_once __DIR__ . '/core/class-mode.php';
@@ -554,6 +555,8 @@ public static function init() {
self::$onboarding_service->init_hooks();
self::$incentives_service->init_hooks();
self::$compatibility_service->init_hooks();
+ self::$customer_service->init_hooks();
+ self::$token_service->init_hooks();
$payment_method_classes = [
CC_Payment_Method::class,
@@ -1877,12 +1880,13 @@ public static function init_woopay() {
public static function load_stripe_bnpl_site_messaging() {
// The messaging element shall not be shown for subscription products.
// As we are not too deep into subscriptions API, we follow simplistic approach for now.
- $is_subscription = false;
- $are_subscriptions_enabled = class_exists( 'WC_Subscriptions' ) || class_exists( 'WC_Subscriptions_Core_Plugin' );
+ $is_subscription = false;
+ $cart_contains_subscription = false;
+ $are_subscriptions_enabled = class_exists( 'WC_Subscriptions' ) || class_exists( 'WC_Subscriptions_Core_Plugin' );
if ( $are_subscriptions_enabled ) {
- global $product;
- $is_subscription = $product && WC_Subscriptions_Product::is_subscription( $product );
- $cart_contains_subscription = is_cart() && WC_Subscriptions_Cart::cart_contains_subscription();
+ global $product;
+ $is_subscription = $product && WC_Subscriptions_Product::is_subscription( $product );
+ $cart_contains_subscription = is_cart() && WC_Subscriptions_Cart::cart_contains_subscription();
}
if ( ! $is_subscription && ! $cart_contains_subscription ) {
diff --git a/includes/class-woopay-tracker.php b/includes/class-woopay-tracker.php
index 538ec873dc8..fbcdd6bc948 100644
--- a/includes/class-woopay-tracker.php
+++ b/includes/class-woopay-tracker.php
@@ -543,7 +543,7 @@ public function checkout_order_processed( $order_id ) {
$properties = [ 'payment_title' => 'other' ];
// If the order was placed using WooCommerce Payments, record the payment title using Tracks.
- if ( strpos( $payment_gateway->id, 'woocommerce_payments' ) === 0 ) {
+ if ( isset( $payment_gateway->id ) && strpos( $payment_gateway->id, 'woocommerce_payments' ) === 0 ) {
$order = wc_get_order( $order_id );
$payment_title = $order->get_payment_method_title();
$properties = [ 'payment_title' => $payment_title ];
diff --git a/includes/compat/blocks/class-blocks-data-extractor.php b/includes/compat/blocks/class-blocks-data-extractor.php
index 673cae7f352..becc393a5da 100644
--- a/includes/compat/blocks/class-blocks-data-extractor.php
+++ b/includes/compat/blocks/class-blocks-data-extractor.php
@@ -59,6 +59,15 @@ private function get_available_blocks() {
$blocks[] = new \Mailchimp_Woocommerce_Newsletter_Blocks_Integration();
}
+ if ( class_exists( '\WCK\Blocks\CheckoutIntegration' ) ) {
+ // phpcs:ignore
+ /**
+ * @psalm-suppress UndefinedClass
+ * @phpstan-ignore-next-line
+ */
+ $blocks[] = new \WCK\Blocks\CheckoutIntegration();
+ }
+
return $blocks;
}
diff --git a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php
index 31ec70bedf8..d2584f9b824 100644
--- a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php
+++ b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php
@@ -11,6 +11,7 @@
use WCPay\Core\Server\Request\Get_Intention;
use WCPay\Exceptions\API_Exception;
+use WCPay\Exceptions\API_Merchant_Exception;
use WCPay\Exceptions\Invalid_Payment_Method_Exception;
use WCPay\Exceptions\Add_Payment_Method_Exception;
use WCPay\Exceptions\Order_Not_Found_Exception;
@@ -342,6 +343,11 @@ public function scheduled_subscription_payment( $amount, $renewal_order ) {
$renewal_order->update_status( 'failed' );
if ( ! empty( $payment_information ) ) {
+ $error_details = esc_html( rtrim( $e->getMessage(), '.' ) );
+ if ( $e instanceof API_Merchant_Exception ) {
+ $error_details = $error_details . '. ' . esc_html( rtrim( $e->get_merchant_message(), '.' ) );
+ }
+
$note = sprintf(
WC_Payments_Utils::esc_interpolated_html(
/* translators: %1: the failed payment amount, %2: error message */
@@ -358,7 +364,7 @@ public function scheduled_subscription_payment( $amount, $renewal_order ) {
wc_price( $amount, [ 'currency' => WC_Payments_Utils::get_order_intent_currency( $renewal_order ) ] ),
$renewal_order
),
- esc_html( rtrim( $e->getMessage(), '.' ) )
+ $error_details
);
$renewal_order->add_order_note( $note );
}
diff --git a/includes/constants/class-express-checkout-hong-kong-states.php b/includes/constants/class-express-checkout-hong-kong-states.php
new file mode 100644
index 00000000000..cd7154eeca0
--- /dev/null
+++ b/includes/constants/class-express-checkout-hong-kong-states.php
@@ -0,0 +1,360 @@
+merchant_message = $merchant_message;
+
+ parent::__construct( $message, $error_code, $http_code, $error_type, $decline_code, $code, $previous );
+ }
+
+ /**
+ * Returns the merchant message.
+ *
+ * @return string Merchant message.
+ */
+ public function get_merchant_message(): string {
+ return $this->merchant_message;
+ }
+}
diff --git a/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php
index 0e54c65f310..d14460da71e 100644
--- a/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php
+++ b/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php
@@ -79,11 +79,11 @@ public function ajax_create_order() {
define( 'WCPAY_ECE_CHECKOUT', true );
}
+ $this->express_checkout_button_helper->normalize_state();
+
// In case the state is required, but is missing, add a more descriptive error notice.
$this->express_checkout_button_helper->validate_state();
- $this->express_checkout_button_helper->normalize_state();
-
WC()->checkout()->process_checkout();
} catch ( Exception $e ) {
Logger::error( 'Failed to process express checkout payment: ' . $e );
@@ -333,6 +333,7 @@ public function ajax_get_selected_product_data() {
$data['needs_shipping'] = wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping();
$data['currency'] = strtolower( get_woocommerce_currency() );
$data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 );
+ $data['has_free_trial'] = class_exists( 'WC_Subscriptions_Product' ) ? WC_Subscriptions_Product::get_trial_length( $product ) > 0 : false;
wp_send_json( $data );
} catch ( Exception $e ) {
diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php
index 285ce659d94..86d1a82c54d 100644
--- a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php
+++ b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php
@@ -415,7 +415,7 @@ public function should_show_express_checkout_button() {
return true;
}
- // Non-shipping product and billing is calculated based on shopper billing addres. Excludes Pay for Order page.
+ // Non-shipping product and tax is calculated based on shopper billing address. Excludes Pay for Order page.
if (
// If the product doesn't needs shipping.
(
@@ -426,8 +426,10 @@ public function should_show_express_checkout_button() {
( ( $this->is_cart() || $this->is_checkout() ) && ! WC()->cart->needs_shipping() )
)
- // ...and billing is calculated based on billing address.
- && wc_tax_enabled() && 'billing' === get_option( 'woocommerce_tax_based_on' )
+ // ...and tax is calculated based on billing address.
+ && wc_tax_enabled()
+ && 'billing' === get_option( 'woocommerce_tax_based_on' )
+ && 'yes' !== get_option( 'woocommerce_prices_include_tax' )
) {
return false;
}
@@ -742,6 +744,7 @@ public function get_product_data() {
$data['needs_shipping'] = ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() );
$data['currency'] = strtolower( $currency );
$data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 );
+ $data['product_type'] = $product->get_type();
return apply_filters( 'wcpay_payment_request_product_data', $data, $product );
}
@@ -760,22 +763,29 @@ private function is_product_supported() {
*
* @psalm-suppress UndefinedClass
*/
- if ( is_null( $product )
- || ! is_object( $product )
- || ! in_array( $product->get_type(), $this->supported_product_types(), true )
- || ( class_exists( 'WC_Subscriptions_Product' ) && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) // Trial subscriptions with shipping are not supported.
+
+ if ( is_null( $product ) || ! is_object( $product ) ) {
+ $is_supported = false;
+ } else {
+ // Simple subscription that needs shipping with free trials is not supported.
+ $is_free_trial_simple_subs = class_exists( 'WC_Subscriptions_Product' ) && $product->get_type() === 'subscription' && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0;
+
+ if (
+ ! in_array( $product->get_type(), $this->supported_product_types(), true )
+ || $is_free_trial_simple_subs
|| ( class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) // Pre Orders charge upon release not supported.
|| ( class_exists( 'WC_Composite_Products' ) && $product->is_type( 'composite' ) ) // Composite products are not supported on the product page.
|| ( class_exists( 'WC_Mix_and_Match' ) && $product->is_type( 'mix-and-match' ) ) // Mix and match products are not supported on the product page.
- ) {
- $is_supported = false;
- } elseif ( class_exists( 'WC_Product_Addons_Helper' ) ) {
- // File upload addon not supported.
- $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() );
- foreach ( $product_addons as $addon ) {
- if ( 'file_upload' === $addon['type'] ) {
- $is_supported = false;
- break;
+ ) {
+ $is_supported = false;
+ } elseif ( class_exists( 'WC_Product_Addons_Helper' ) ) {
+ // File upload addon not supported.
+ $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() );
+ foreach ( $product_addons as $addon ) {
+ if ( 'file_upload' === $addon['type'] ) {
+ $is_supported = false;
+ break;
+ }
}
}
}
@@ -946,6 +956,46 @@ public function normalize_state() {
$billing_state = ! empty( $_POST['billing_state'] ) ? wc_clean( wp_unslash( $_POST['billing_state'] ) ) : '';
$shipping_state = ! empty( $_POST['shipping_state'] ) ? wc_clean( wp_unslash( $_POST['shipping_state'] ) ) : '';
+ // Due to a bug in Apple Pay, the "Region" part of a Hong Kong address is delivered in
+ // `shipping_postcode`, so we need some special case handling for that. According to
+ // our sources at Apple Pay people will sometimes use the district or even sub-district
+ // for this value. As such we check against all regions, districts, and sub-districts
+ // with both English and Mandarin spelling.
+ //
+ // @reykjalin: The check here is quite elaborate in an attempt to make sure this doesn't break once
+ // Apple Pay fixes the bug that causes address values to be in the wrong place. Because of that the
+ // algorithm becomes:
+ // 1. Use the supplied state if it's valid (in case Apple Pay bug is fixed)
+ // 2. Use the value supplied in the postcode if it's a valid HK region (equivalent to a WC state).
+ // 3. Fall back to the value supplied in the state. This will likely cause a validation error, in
+ // which case a merchant can reach out to us so we can either: 1) add whatever the customer used
+ // as a state to our list of valid states; or 2) let them know the customer must spell the state
+ // in some way that matches our list of valid states.
+ //
+ // @reykjalin: This HK specific sanitazation *should be removed* once Apple Pay fix
+ // the address bug. More info on that in pc4etw-bY-p2.
+ if ( 'HK' === $billing_country ) {
+ include_once WCPAY_ABSPATH . 'includes/constants/class-express-checkout-hong-kong-states.php';
+
+ if ( ! \WCPay\Constants\Express_Checkout_Hong_Kong_States::is_valid_state( strtolower( $billing_state ) ) ) {
+ $billing_postcode = ! empty( $_POST['billing_postcode'] ) ? wc_clean( wp_unslash( $_POST['billing_postcode'] ) ) : '';
+ if ( \WCPay\Constants\Express_Checkout_Hong_Kong_States::is_valid_state( strtolower( $billing_postcode ) ) ) {
+ $billing_state = $billing_postcode;
+ }
+ }
+ }
+ if ( 'HK' === $shipping_country ) {
+ include_once WCPAY_ABSPATH . 'includes/constants/class-express-checkout-hong-kong-states.php';
+
+ if ( ! \WCPay\Constants\Express_Checkout_Hong_Kong_States::is_valid_state( strtolower( $shipping_state ) ) ) {
+ $shipping_postcode = ! empty( $_POST['shipping_postcode'] ) ? wc_clean( wp_unslash( $_POST['shipping_postcode'] ) ) : '';
+ if ( \WCPay\Constants\Express_Checkout_Hong_Kong_States::is_valid_state( strtolower( $shipping_postcode ) ) ) {
+ $shipping_state = $shipping_postcode;
+ }
+ }
+ }
+
+ // Finally we normalize the state value we want to process.
if ( $billing_state && $billing_country ) {
$_POST['billing_state'] = $this->get_normalized_state( $billing_state, $billing_country );
}
diff --git a/includes/multi-currency/Compatibility/WooCommerceFedEx.php b/includes/multi-currency/Compatibility/WooCommerceFedEx.php
index 8a38d058e40..15c25b4ba27 100644
--- a/includes/multi-currency/Compatibility/WooCommerceFedEx.php
+++ b/includes/multi-currency/Compatibility/WooCommerceFedEx.php
@@ -8,13 +8,25 @@
namespace WCPay\MultiCurrency\Compatibility;
use WCPay\MultiCurrency\MultiCurrency;
-use WCPay\MultiCurrency\Utils;
/**
* Class that controls Multi Currency Compatibility with WooCommerce FedEx Plugin.
*/
class WooCommerceFedEx extends BaseCompatibility {
+ /**
+ * Calls to look for in the backtrace when determining whether
+ * to return store currency or skip converting product prices.
+ */
+ private const WC_SHIPPING_FEDEX_CALLS = [
+ 'WC_Shipping_Fedex->set_settings',
+ 'WC_Shipping_Fedex->per_item_shipping',
+ 'WC_Shipping_Fedex->box_shipping',
+ 'WC_Shipping_Fedex->get_fedex_api_request',
+ 'WC_Shipping_Fedex->get_fedex_requests',
+ 'WC_Shipping_Fedex->process_result',
+ ];
+
/**
* Init the class.
*
@@ -23,10 +35,31 @@ class WooCommerceFedEx extends BaseCompatibility {
public function init() {
// Add needed actions and filters if FedEx is active.
if ( class_exists( 'WC_Shipping_Fedex_Init' ) ) {
+ add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ] );
add_filter( MultiCurrency::FILTER_PREFIX . 'should_return_store_currency', [ $this, 'should_return_store_currency' ] );
}
}
+ /**
+ * Checks to see if the product's price should be converted.
+ *
+ * @param bool $return Whether to convert the product's price or not. Default is true.
+ *
+ * @return bool True if it should be converted.
+ */
+ public function should_convert_product_price( bool $return ): bool {
+ // If it's already false, return it.
+ if ( ! $return ) {
+ return $return;
+ }
+
+ if ( $this->utils->is_call_in_backtrace( self::WC_SHIPPING_FEDEX_CALLS ) ) {
+ return false;
+ }
+
+ return $return;
+ }
+
/**
* Determine whether to return the store currency or not.
*
@@ -40,15 +73,7 @@ public function should_return_store_currency( bool $return ): bool {
return $return;
}
- $calls = [
- 'WC_Shipping_Fedex->set_settings',
- 'WC_Shipping_Fedex->per_item_shipping',
- 'WC_Shipping_Fedex->box_shipping',
- 'WC_Shipping_Fedex->get_fedex_api_request',
- 'WC_Shipping_Fedex->get_fedex_requests',
- 'WC_Shipping_Fedex->process_result',
- ];
- if ( $this->utils->is_call_in_backtrace( $calls ) ) {
+ if ( $this->utils->is_call_in_backtrace( self::WC_SHIPPING_FEDEX_CALLS ) ) {
return true;
}
diff --git a/includes/multi-currency/MultiCurrency.php b/includes/multi-currency/MultiCurrency.php
index 301503d9ca0..9ab1ac0f19a 100644
--- a/includes/multi-currency/MultiCurrency.php
+++ b/includes/multi-currency/MultiCurrency.php
@@ -832,7 +832,12 @@ public function get_price( $price, string $type ): float {
return (float) $price;
}
+ // We must ceil the converted price here so that we don't introduce rounding errors when
+ // summing up costs. Consider, e.g. a converted price of 10.003 for a 2-decimal currency.
+ // A single product would cost 10.00, but 2 of them would cost 20.01, _unless_ we round
+ // the individual parts correctly.
$converted_price = ( (float) $price ) * $currency->get_rate();
+ $converted_price = $this->ceil_price_for_currency( $converted_price, $currency );
if ( 'tax' === $type || 'coupon' === $type || 'exchange_rate' === $type ) {
return $converted_price;
@@ -1356,6 +1361,39 @@ protected function ceil_price( float $price, float $rounding ): float {
return ceil( $price / $rounding ) * $rounding;
}
+ /**
+ * Ceils the price to the precision dictated by the number of decimals in the provided currency.
+ *
+ * For example: US$10.0091 -> US$10.01, JPY 1001.01 -> JPY 1002.
+ *
+ * @param float $price The price to be ceiled.
+ * @param Currency $currency The currency used to figure out the ceil precision.
+ *
+ * @return float The ceiled price.
+ */
+ protected function ceil_price_for_currency( float $price, Currency $currency ): float {
+ // phpcs:disable Squiz.PHP.CommentedOutCode.Found, example comments look like code.
+
+ // Example to explain the math:
+ // $price = 10.003.
+ // expected rounding = 10.01.
+
+ // $num_decimals = 2.
+ // $factor. = 10^2 = 100.
+ $num_decimals = absint(
+ $this->localization_service->get_currency_format(
+ $currency->get_code()
+ )['num_decimals']
+ );
+ $factor = 10 ** $num_decimals; // 10^{$num_decimals}.
+
+ // ceil( 10.003 * $factor ) = ceil( 1_000.3 ) = 1_001.
+ // 1_001 / 100 = 10.01.
+ return ceil( $price * $factor ) / $factor; // = 10.01.
+
+ // phpcs:enable Squiz.PHP.CommentedOutCode.Found
+ }
+
/**
* Sets up the available currencies, which are alphabetical by name.
*
diff --git a/includes/payment-methods/class-affirm-payment-method.php b/includes/payment-methods/class-affirm-payment-method.php
index 47b89f49951..1c87c67149f 100644
--- a/includes/payment-methods/class-affirm-payment-method.php
+++ b/includes/payment-methods/class-affirm-payment-method.php
@@ -27,7 +27,6 @@ class Affirm_Payment_Method extends UPE_Payment_Method {
public function __construct( $token_service ) {
parent::__construct( $token_service );
$this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID;
- $this->title = __( 'Affirm', 'woocommerce-payments' );
$this->is_reusable = false;
$this->is_bnpl = true;
$this->icon_url = plugins_url( 'assets/images/payment-methods/affirm-logo.svg', WCPAY_PLUGIN_FILE );
@@ -38,6 +37,18 @@ public function __construct( $token_service ) {
$this->countries = [ Country_Code::UNITED_STATES, Country_Code::CANADA ];
}
+ /**
+ * Returns payment method title
+ *
+ * @param string|null $account_country Country of merchants account.
+ * @param array|false $payment_details Optional payment details from charge object.
+ *
+ * @return string
+ */
+ public function get_title( ?string $account_country = null, $payment_details = false ) {
+ return __( 'Affirm', 'woocommerce-payments' );
+ }
+
/**
* Returns testing credentials to be printed at checkout in test mode.
*
diff --git a/includes/payment-methods/class-afterpay-payment-method.php b/includes/payment-methods/class-afterpay-payment-method.php
index 3674731835c..503f0c6104d 100644
--- a/includes/payment-methods/class-afterpay-payment-method.php
+++ b/includes/payment-methods/class-afterpay-payment-method.php
@@ -27,7 +27,6 @@ class Afterpay_Payment_Method extends UPE_Payment_Method {
public function __construct( $token_service ) {
parent::__construct( $token_service );
$this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID;
- $this->title = __( 'Afterpay', 'woocommerce-payments' );
$this->is_reusable = false;
$this->is_bnpl = true;
$this->icon_url = plugins_url( 'assets/images/payment-methods/afterpay-logo.svg', WCPAY_PLUGIN_FILE );
diff --git a/includes/payment-methods/class-cc-payment-method.php b/includes/payment-methods/class-cc-payment-method.php
index 50a44fa1114..58d7d733a77 100644
--- a/includes/payment-methods/class-cc-payment-method.php
+++ b/includes/payment-methods/class-cc-payment-method.php
@@ -25,7 +25,6 @@ class CC_Payment_Method extends UPE_Payment_Method {
public function __construct( $token_service ) {
parent::__construct( $token_service );
$this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID;
- $this->title = __( 'Credit card / debit card', 'woocommerce-payments' );
$this->is_reusable = true;
$this->currencies = [];// All currencies are supported.
$this->icon_url = plugins_url( 'assets/images/payment-methods/generic-card.svg', WCPAY_PLUGIN_FILE );
@@ -40,7 +39,7 @@ public function __construct( $token_service ) {
*/
public function get_title( ?string $account_country = null, $payment_details = false ) {
if ( ! $payment_details ) {
- return $this->title;
+ return __( 'Credit card / debit card', 'woocommerce-payments' );
}
$details = $payment_details[ $this->stripe_id ];
diff --git a/includes/payment-methods/class-klarna-payment-method.php b/includes/payment-methods/class-klarna-payment-method.php
index 31c71cb813a..27495db4b02 100644
--- a/includes/payment-methods/class-klarna-payment-method.php
+++ b/includes/payment-methods/class-klarna-payment-method.php
@@ -27,7 +27,6 @@ class Klarna_Payment_Method extends UPE_Payment_Method {
public function __construct( $token_service ) {
parent::__construct( $token_service );
$this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID;
- $this->title = __( 'Klarna', 'woocommerce-payments' );
$this->is_reusable = false;
$this->is_bnpl = true;
$this->icon_url = plugins_url( 'assets/images/payment-methods/klarna-pill.svg', WCPAY_PLUGIN_FILE );
@@ -37,6 +36,18 @@ public function __construct( $token_service ) {
$this->limits_per_currency = WC_Payments_Utils::get_bnpl_limits_per_currency( self::PAYMENT_METHOD_STRIPE_ID );
}
+ /**
+ * Returns payment method title
+ *
+ * @param string|null $account_country Country of merchants account.
+ * @param array|false $payment_details Optional payment details from charge object.
+ *
+ * @return string
+ */
+ public function get_title( ?string $account_country = null, $payment_details = false ) {
+ return __( 'Klarna', 'woocommerce-payments' );
+ }
+
/**
* Returns payment method supported countries.
*
diff --git a/includes/payment-methods/class-link-payment-method.php b/includes/payment-methods/class-link-payment-method.php
index c5c189bbad8..0e086cd7e86 100644
--- a/includes/payment-methods/class-link-payment-method.php
+++ b/includes/payment-methods/class-link-payment-method.php
@@ -25,12 +25,23 @@ class Link_Payment_Method extends UPE_Payment_Method {
public function __construct( $token_service ) {
parent::__construct( $token_service );
$this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID;
- $this->title = __( 'Link', 'woocommerce-payments' );
$this->is_reusable = true;
$this->currencies = [ Currency_Code::UNITED_STATES_DOLLAR ];
$this->icon_url = plugins_url( 'assets/images/payment-methods/link.svg', WCPAY_PLUGIN_FILE );
}
+ /**
+ * Returns payment method title
+ *
+ * @param string|null $account_country Country of merchants account.
+ * @param array|false $payment_details Optional payment details from charge object.
+ *
+ * @return string
+ */
+ public function get_title( ?string $account_country = null, $payment_details = false ) {
+ return __( 'Link', 'woocommerce-payments' );
+ }
+
/**
* Returns testing credentials to be printed at checkout in test mode.
*
diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php
index 01c57eb8969..e90094d57de 100644
--- a/includes/wc-payment-api/class-wc-payments-api-client.php
+++ b/includes/wc-payment-api/class-wc-payments-api-client.php
@@ -9,6 +9,7 @@
use WCPay\Constants\Intent_Status;
use WCPay\Exceptions\API_Exception;
+use WCPay\Exceptions\API_Merchant_Exception;
use WCPay\Exceptions\Amount_Too_Small_Exception;
use WCPay\Exceptions\Amount_Too_Large_Exception;
use WCPay\Exceptions\Connection_Exception;
@@ -81,6 +82,7 @@ class WC_Payments_API_Client implements MultiCurrencyApiClientInterface {
const FRAUD_RULESET_API = 'fraud_ruleset';
const COMPATIBILITY_API = 'compatibility';
const REPORTING_API = 'reporting/payment_activity';
+ const RECOMMENDED_PAYMENT_METHODS = 'payment_methods/recommended';
/**
* Common keys in API requests/responses that we might want to redact.
@@ -453,6 +455,65 @@ public function get_transactions_export( $filters = [], $user_email = '', $depos
return $this->request( $filters, self::TRANSACTIONS_API . '/download', self::POST );
}
+ /**
+ * Fetch account recommended payment methods data for a given country.
+ *
+ * @param string $country_code The account's business location country code. Provide a 2-letter ISO country code.
+ * @param string $locale Optional. The locale to instruct the platform to use for i18n.
+ *
+ * @return array The recommended payment methods data.
+ * @throws API_Exception Exception thrown on request failure.
+ */
+ public function get_recommended_payment_methods( string $country_code, string $locale = '' ): array {
+ // We can't use the request method here because this route doesn't require a connected store
+ // and we request this data pre-onboarding.
+ // By this point, we have an expired transient or the store context has changed.
+ // Query for incentives by calling the WooPayments API.
+ $url = add_query_arg(
+ [
+ 'country_code' => $country_code,
+ 'locale' => $locale,
+ ],
+ self::ENDPOINT_BASE . '/' . self::ENDPOINT_REST_BASE . '/' . self::RECOMMENDED_PAYMENT_METHODS,
+ );
+
+ $response = wp_remote_get(
+ $url,
+ [
+ 'headers' => apply_filters(
+ 'wcpay_api_request_headers',
+ [
+ 'Content-type' => 'application/json; charset=utf-8',
+ ]
+ ),
+ 'user-agent' => $this->user_agent,
+ 'timeout' => self::API_TIMEOUT_SECONDS,
+ 'sslverify' => false,
+ ]
+ );
+
+ if ( is_wp_error( $response ) ) {
+ Logger::error( 'HTTP_REQUEST_ERROR ' . var_export( $response, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
+ $message = sprintf(
+ // translators: %1: original error message.
+ __( 'Http request failed. Reason: %1$s', 'woocommerce-payments' ),
+ $response->get_error_message()
+ );
+ throw new API_Exception( $message, 'wcpay_http_request_failed', 500 );
+ }
+
+ $results = [];
+ if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
+ // Decode the results, falling back to an empty array.
+ $results = $this->extract_response_body( $response );
+ if ( ! is_array( $results ) ) {
+ $results = [];
+ }
+ }
+
+ return $results;
+ }
+
/**
* Fetch a single transaction with provided id.
*
@@ -2359,6 +2420,13 @@ protected function check_response_for_errors( $response ) {
);
Logger::error( "$error_message ($error_code)" );
+
+ if ( 'card_declined' === $error_code && isset( $response_body['error']['payment_intent']['charges']['data'][0]['outcome']['seller_message'] ) ) {
+ $merchant_message = $response_body['error']['payment_intent']['charges']['data'][0]['outcome']['seller_message'];
+
+ throw new API_Merchant_Exception( $message, $error_code, $response_code, $merchant_message, $error_type, $decline_code );
+ }
+
throw new API_Exception( $message, $error_code, $response_code, $error_type, $decline_code );
}
}
diff --git a/includes/wc-payment-api/class-wc-payments-http.php b/includes/wc-payment-api/class-wc-payments-http.php
index 65081e8be10..1dc14048cfb 100644
--- a/includes/wc-payment-api/class-wc-payments-http.php
+++ b/includes/wc-payment-api/class-wc-payments-http.php
@@ -199,7 +199,8 @@ public function start_connection( $redirect ) {
wp_safe_redirect(
add_query_arg(
[
- 'from' => 'woocommerce-payments',
+ 'from' => 'woocommerce-core-profiler',
+ 'plugin_name' => 'woocommerce-payments',
'calypso_env' => $calypso_env,
],
$this->connection_manager->get_authorization_url( null, $redirect )
diff --git a/package-lock.json b/package-lock.json
index 4d8d73b8e87..083ed1adf12 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "woocommerce-payments",
- "version": "8.6.0",
+ "version": "8.6.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "woocommerce-payments",
- "version": "8.6.0",
+ "version": "8.6.1",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
diff --git a/package.json b/package.json
index 4fa803a245c..f634378e064 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "woocommerce-payments",
- "version": "8.6.0",
+ "version": "8.6.1",
"main": "webpack.config.js",
"author": "Automattic",
"license": "GPL-3.0-or-later",
@@ -69,8 +69,8 @@
"format:css": "npm run format:provided '**/*.scss' '**/*.css'",
"format:provided": "prettier --write",
"tube:setup": "./bin/jurassic-tube-setup.sh",
- "tube:start": "./docker/bin/jt/tunnel.sh",
- "tube:stop": "./docker/bin/jt/tunnel.sh break",
+ "tube:start": "source ./bin/jurassictube/config.env && jurassictube -u \"$username\" -s \"$subdomain\" -h \"$localhost\"",
+ "tube:stop": "source ./bin/jurassictube/config.env && jurassictube -b -s \"$subdomain\"",
"psalm": "./bin/run-psalm.sh",
"xdebug:toggle": "docker compose exec -u root wordpress /var/www/html/wp-content/plugins/woocommerce-payments/bin/xdebug-toggle.sh",
"changelog": "./vendor/bin/changelogger add",
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 2a5164e8af3..d0692a8a2b8 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -24,13 +24,6 @@
WC_Pre_Orders_Product
-
-
- WC_Pre_Orders_Product
- WC_Subscriptions_Product
- WC_Subscriptions_Cart
-
-
WC_Subscriptions_Cart
diff --git a/readme.txt b/readme.txt
index d67f01c3951..0122e5021ae 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: woocommerce payments, apple pay, credit card, google pay, payment, payment
Requires at least: 6.0
Tested up to: 6.7
Requires PHP: 7.3
-Stable tag: 8.6.0
+Stable tag: 8.6.1
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -94,6 +94,11 @@ Please note that our support for the checkout block is still experimental and th
== Changelog ==
+= 8.6.1 - 2024-12-17 =
+* Fix - Checkout: Fix error when wc_address_i18n_params does not have data for a given country
+* Fix - Skip mysqlcheck SSL Requirement during E2E environment setup
+
+
= 8.6.0 - 2024-12-04 =
* Add - Add Bank reference key column in Payout reports. This will help reconcile WooPayments Payouts with bank statements.
* Add - Display credit card brand icons on order received page.
diff --git a/src/Internal/Service/Level3Service.php b/src/Internal/Service/Level3Service.php
index 67db748debe..b75f3dd1271 100644
--- a/src/Internal/Service/Level3Service.php
+++ b/src/Internal/Service/Level3Service.php
@@ -80,10 +80,10 @@ public function get_data_from_order( int $order_id ): array {
$order_items = array_values( $order->get_items( [ 'line_item', 'fee' ] ) );
$currency = $order->get_currency();
- $process_item = function ( $item ) use ( $currency ) {
- return $this->process_item( $item, $currency );
- };
- $items_to_send = array_map( $process_item, $order_items );
+ $items_to_send = [];
+ foreach ( $order_items as $item ) {
+ $items_to_send = array_merge( $items_to_send, $this->process_item( $item, $currency ) );
+ }
$level3_data = [
'merchant_reference' => (string) $order->get_id(), // An alphanumeric string of up to characters in length. This unique value is assigned by the merchant to identify the order. Also known as an “Order ID”.
@@ -137,9 +137,9 @@ public function get_data_from_order( int $order_id ): array {
*
* @param WC_Order_Item_Product|WC_Order_Item_Fee $item Item to process.
* @param string $currency Currency to use.
- * @return \stdClass
+ * @return \stdClass[]
*/
- private function process_item( WC_Order_Item $item, string $currency ): stdClass {
+ private function process_item( WC_Order_Item $item, string $currency ): array {
// Check to see if it is a WC_Order_Item_Product or a WC_Order_Item_Fee.
if ( $item instanceof WC_Order_Item_Product ) {
$subtotal = $item->get_subtotal();
@@ -164,7 +164,7 @@ private function process_item( WC_Order_Item $item, string $currency ): stdClass
$unit_cost = 0;
}
- return (object) [
+ $line_item = (object) [
'product_code' => (string) $product_code, // Up to 12 characters that uniquely identify the product.
'product_description' => $description, // Up to 26 characters long describing the product.
'unit_cost' => $unit_cost, // Cost of the product, in cents, as a non-negative integer.
@@ -172,6 +172,29 @@ private function process_item( WC_Order_Item $item, string $currency ): stdClass
'tax_amount' => $tax_amount, // The amount of tax this item had added to it, in cents, as a non-negative integer.
'discount_amount' => $discount_amount, // The amount an item was discounted—if there was a sale,for example, as a non-negative integer.
];
+ $line_items = [ $line_item ];
+
+ /**
+ * In edge cases, rounding after division might lead to a slight inconsistency.
+ *
+ * For example: 10/3 with 2 decimal places = 3.33, but 3.33*3 = 9.99.
+ */
+ if ( $subtotal > 0 ) {
+ $prepared_subtotal = $this->prepare_amount( $subtotal, $currency );
+ $difference = $prepared_subtotal - ( $unit_cost * $quantity );
+ if ( $difference > 0 ) {
+ $line_items[] = (object) [
+ 'product_code' => 'rounding-fix',
+ 'product_description' => __( 'Rounding fix', 'woocommerce-payments' ),
+ 'unit_cost' => $difference,
+ 'quantity' => 1,
+ 'tax_amount' => 0,
+ 'discount_amount' => 0,
+ ];
+ }
+ }
+
+ return $line_items;
}
/**
diff --git a/tests/e2e/env/setup.sh b/tests/e2e/env/setup.sh
index d2aa3a50e89..5ab08183bac 100755
--- a/tests/e2e/env/setup.sh
+++ b/tests/e2e/env/setup.sh
@@ -123,11 +123,11 @@ step "Setting up CLIENT site"
# Wait for containers to be started up before the setup.
# The db being accessible means that the db container started and the WP has been downloaded and the plugin linked
set +e
-cli wp db check --path=/var/www/html --quiet > /dev/null
+cli wp db check --skip_ssl --path=/var/www/html --quiet > /dev/null
while [[ $? -ne 0 ]]; do
echo "Waiting until the service is ready..."
sleep 5
- cli wp db check --path=/var/www/html --quiet > /dev/null
+ cli wp db check --skip_ssl --path=/var/www/html --quiet > /dev/null
done
echo "Client DB is up and running..."
set -e
diff --git a/tests/fixtures/captured-payments/discount.json b/tests/fixtures/captured-payments/discount.json
index 2fa6a911d74..5bf6f936c45 100644
--- a/tests/fixtures/captured-payments/discount.json
+++ b/tests/fixtures/captured-payments/discount.json
@@ -60,7 +60,7 @@
"feeBreakdown": {
"base": "Base fee: 2.9% + $0.30",
"additional-international": "International card fee: 1%",
- "additional-fx": "Foreign exchange fee: 1%",
+ "additional-fx": "Currency conversion fee: 1%",
"discount": {
"label": "Discount",
"variable": "Variable fee: -4.9%",
diff --git a/tests/fixtures/captured-payments/foreign-card.json b/tests/fixtures/captured-payments/foreign-card.json
index 234878b2372..df45c326d62 100644
--- a/tests/fixtures/captured-payments/foreign-card.json
+++ b/tests/fixtures/captured-payments/foreign-card.json
@@ -53,7 +53,7 @@
"feeBreakdown": {
"base": "Base fee: 2.9% + $0.30",
"additional-international": "International card fee: 1%",
- "additional-fx": "Foreign exchange fee: 1%"
+ "additional-fx": "Currency conversion fee: 1%"
},
"netString": "Net payout: $95.47 USD"
}
diff --git a/tests/fixtures/captured-payments/fx-decimal.json b/tests/fixtures/captured-payments/fx-decimal.json
index b95e9318c84..2f065036122 100644
--- a/tests/fixtures/captured-payments/fx-decimal.json
+++ b/tests/fixtures/captured-payments/fx-decimal.json
@@ -45,7 +45,7 @@
"feeString": "Fee (3.9% + $0.30): -$4.39",
"feeBreakdown": {
"base": "Base fee: 2.9% + $0.30",
- "additional-fx": "Foreign exchange fee: 1%"
+ "additional-fx": "Currency conversion fee: 1%"
},
"netString": "Net payout: $100.65 USD"
}
diff --git a/tests/fixtures/captured-payments/fx-partial-capture.json b/tests/fixtures/captured-payments/fx-partial-capture.json
index f10ff7aa9e9..691390d4852 100644
--- a/tests/fixtures/captured-payments/fx-partial-capture.json
+++ b/tests/fixtures/captured-payments/fx-partial-capture.json
@@ -57,7 +57,7 @@
"feeString": "Fee (3.51% + £0.21): -$0.88",
"feeBreakdown": {
"base": "Base fee: 2.9% + $0.30",
- "additional-fx": "Foreign exchange fee: 1%",
+ "additional-fx": "Currency conversion fee: 1%",
"discount": {
"label": "Discount",
"variable": "Variable fee: -0.39%",
diff --git a/tests/fixtures/captured-payments/fx-with-capped-fee.json b/tests/fixtures/captured-payments/fx-with-capped-fee.json
index 8c1b602a3eb..4c31a8435d7 100644
--- a/tests/fixtures/captured-payments/fx-with-capped-fee.json
+++ b/tests/fixtures/captured-payments/fx-with-capped-fee.json
@@ -55,7 +55,7 @@
"feeBreakdown": {
"base": "Base fee: capped at $6.00",
"additional-international": "International card fee: 1.5%",
- "additional-fx": "Foreign exchange fee: 1%"
+ "additional-fx": "Currency conversion fee: 1%"
},
"netString": "Net payout: $971.04 USD"
}
diff --git a/tests/fixtures/captured-payments/fx.json b/tests/fixtures/captured-payments/fx.json
index 8ceee7b7438..f18ca9297ab 100644
--- a/tests/fixtures/captured-payments/fx.json
+++ b/tests/fixtures/captured-payments/fx.json
@@ -46,7 +46,7 @@
"feeString": "Fee (3.9% + $0.30): -$4.20",
"feeBreakdown": {
"base": "Base fee: 2.9% + $0.30",
- "additional-fx": "Foreign exchange fee: 1%"
+ "additional-fx": "Currency conversion fee: 1%"
},
"netString": "Net payout: $95.84 USD"
}
diff --git a/tests/fixtures/captured-payments/jpy-payment.json b/tests/fixtures/captured-payments/jpy-payment.json
index 6c7a6b3ee05..4b4c6c152c9 100644
--- a/tests/fixtures/captured-payments/jpy-payment.json
+++ b/tests/fixtures/captured-payments/jpy-payment.json
@@ -57,7 +57,7 @@
"feeBreakdown": {
"base": "Base fee: 3.6%",
"additional-international": "International card fee: 2%",
- "additional-fx": "Foreign exchange fee: 2%"
+ "additional-fx": "Currency conversion fee: 2%"
},
"netString": "Net payout: ¥4,507 JPY"
}
diff --git a/tests/fixtures/captured-payments/subscription.json b/tests/fixtures/captured-payments/subscription.json
index b7312ea0c02..d0e1fe705e4 100644
--- a/tests/fixtures/captured-payments/subscription.json
+++ b/tests/fixtures/captured-payments/subscription.json
@@ -53,7 +53,7 @@
"feeString": "Fee (4.9% + $0.30): -$3.04",
"feeBreakdown": {
"base": "Base fee: 2.9% + $0.30",
- "additional-fx": "Foreign exchange fee: 1%",
+ "additional-fx": "Currency conversion fee: 1%",
"additional-wcpay-subscription": "Subscription transaction fee: 1%"
},
"netString": "Net payout: $52.87 USD"
diff --git a/tests/js/jest.config.js b/tests/js/jest.config.js
index b81f434b8c5..7a918a8d63c 100644
--- a/tests/js/jest.config.js
+++ b/tests/js/jest.config.js
@@ -45,8 +45,6 @@ module.exports = {
'/.*/build-module/',
'/docker/',
'/tests/e2e',
- // We'll delete the directory and its contents as part of https://github.com/Automattic/woocommerce-payments/issues/9722 .
- '/client/tokenized-payment-request',
],
transform: {
...tsjPreset.transform,
diff --git a/tests/unit/admin/tasks/test-class-wc-payments-task-disputes.php b/tests/unit/admin/tasks/test-class-wc-payments-task-disputes.php
index 43771de278d..729be9743f0 100644
--- a/tests/unit/admin/tasks/test-class-wc-payments-task-disputes.php
+++ b/tests/unit/admin/tasks/test-class-wc-payments-task-disputes.php
@@ -8,6 +8,7 @@
use WCPay\Constants\Country_Code;
use WooCommerce\Payments\Tasks\WC_Payments_Task_Disputes;
+require_once WCPAY_ABSPATH . 'includes/admin/tasks/class-wc-payments-task-disputes.php';
/**
* WC_Payments_Task_Disputes unit tests.
*/
diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php
index 6a16577f18c..6dba99d9d1b 100644
--- a/tests/unit/admin/test-class-wc-payments-admin.php
+++ b/tests/unit/admin/test-class-wc-payments-admin.php
@@ -203,6 +203,8 @@ private function mock_current_user_is_admin() {
*/
public function test_maybe_redirect_from_payments_admin_child_pages( $expected_times_redirect_called, $has_working_jetpack_connection, $is_stripe_account_valid, $get_params ) {
$this->mock_current_user_is_admin();
+ $this->payments_admin->add_payments_menu();
+
$_GET = $get_params;
$this->mock_account
diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php
index 99f99b071c2..89ef79bfb11 100755
--- a/tests/unit/bootstrap.php
+++ b/tests/unit/bootstrap.php
@@ -96,8 +96,6 @@ function () {
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-customer-controller.php';
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-refunds-controller.php';
- require_once $_plugin_dir . 'includes/class-wc-payments-payment-request-button-handler.php';
-
// Load currency helper class early to ensure its implementation is used over the one resolved during further test initialization.
require_once __DIR__ . '/helpers/class-wc-helper-site-currency.php';
diff --git a/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-handler.php b/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-handler.php
new file mode 100644
index 00000000000..0b10752c0f5
--- /dev/null
+++ b/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-handler.php
@@ -0,0 +1,136 @@
+shipping()->unregister_shipping_methods();
+
+ $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
+ $this->mock_wcpay_gateway = $this->createMock( WC_Payment_Gateway_WCPay::class );
+ $this->mock_ece_button_helper = $this->createMock( WC_Payments_Express_Checkout_Button_Helper::class );
+ $this->mock_express_checkout_ajax_handler = $this->createMock( WC_Payments_Express_Checkout_Ajax_Handler::class );
+
+ $this->system_under_test = new WC_Payments_Express_Checkout_Button_Handler(
+ $this->mock_wcpay_account,
+ $this->mock_wcpay_gateway,
+ $this->mock_ece_button_helper,
+ $this->mock_express_checkout_ajax_handler
+ );
+
+ // Set up shipping zones and methods.
+ $this->zone = new WC_Shipping_Zone();
+ $this->zone->set_zone_name( 'Worldwide' );
+ $this->zone->set_zone_order( 1 );
+ $this->zone->save();
+
+ $flat_rate = $this->zone->add_shipping_method( 'flat_rate' );
+ $this->flat_rate_id = $flat_rate;
+
+ $local_pickup = $this->zone->add_shipping_method( 'local_pickup' );
+ $this->local_pickup_id = $local_pickup;
+ }
+
+ public function tear_down() {
+ parent::tear_down();
+
+ // Clean up shipping zones and methods.
+ $this->zone->delete();
+ }
+
+ public function test_filter_cart_needs_shipping_address_regular_products() {
+ $this->assertEquals(
+ true,
+ $this->system_under_test->filter_cart_needs_shipping_address( true ),
+ 'Should not modify shipping address requirement for regular products'
+ );
+ }
+
+
+ public function test_filter_cart_needs_shipping_address_subscription_products() {
+ WC_Subscriptions_Cart::set_cart_contains_subscription( true );
+ $this->mock_ece_button_helper->method( 'is_checkout' )->willReturn( true );
+
+ $this->zone->delete_shipping_method( $this->flat_rate_id );
+ $this->zone->delete_shipping_method( $this->local_pickup_id );
+
+ $this->assertFalse(
+ $this->system_under_test->filter_cart_needs_shipping_address( true ),
+ 'Should not require shipping address for subscription without shipping methods'
+ );
+
+ remove_filter( 'woocommerce_shipping_method_count', '__return_zero' );
+ WC_Subscriptions_Cart::set_cart_contains_subscription( false );
+ }
+}
diff --git a/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php b/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php
index 2432c61172c..8006faac78f 100644
--- a/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php
+++ b/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php
@@ -28,13 +28,6 @@ class WC_Payments_Express_Checkout_Button_Helper_Test extends WCPAY_UnitTestCase
*/
private $mock_wcpay_account;
- /**
- * Express Checkout Helper instance.
- *
- * @var WC_Payments_Express_Checkout_Button_Helper
- */
- private $express_checkout_helper;
-
/**
* Test shipping zone.
*
@@ -61,21 +54,7 @@ class WC_Payments_Express_Checkout_Button_Helper_Test extends WCPAY_UnitTestCase
*
* @var WC_Payments_Express_Checkout_Button_Helper
*/
- private $mock_express_checkout_helper;
-
- /**
- * Express Checkout Ajax Handler instance.
- *
- * @var WC_Payments_Express_Checkout_Ajax_Handler
- */
- private $mock_express_checkout_ajax_handler;
-
- /**
- * Express Checkout ECE Button Handler instance.
- *
- * @var WC_Payments_Express_Checkout_Button_Handler
- */
- private $mock_express_checkout_ece_button_handler;
+ private $system_under_test;
/**
* Test product to add to the cart
@@ -92,23 +71,7 @@ public function set_up() {
$this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
$this->mock_wcpay_gateway = $this->make_wcpay_gateway();
- $this->mock_express_checkout_helper = new WC_Payments_Express_Checkout_Button_Helper( $this->mock_wcpay_gateway, $this->mock_wcpay_account );
- $this->mock_express_checkout_ajax_handler = $this->getMockBuilder( WC_Payments_Express_Checkout_Ajax_Handler::class )
- ->setConstructorArgs(
- [
- $this->mock_express_checkout_helper,
- ]
- )
- ->getMock();
-
- $this->mock_ece_button_helper = $this->getMockBuilder( WC_Payments_Express_Checkout_Button_Helper::class )
- ->setConstructorArgs(
- [
- $this->mock_wcpay_gateway,
- $this->mock_wcpay_account,
- ]
- )
- ->getMock();
+ $this->system_under_test = new WC_Payments_Express_Checkout_Button_Helper( $this->mock_wcpay_gateway, $this->mock_wcpay_account );
WC_Helper_Shipping::delete_simple_flat_rate();
$zone = new WC_Shipping_Zone();
@@ -128,7 +91,7 @@ public function set_up() {
WC()->session->init();
WC()->cart->add_to_cart( $this->simple_product->get_id(), 1 );
- $this->mock_express_checkout_helper->update_shipping_method( [ self::get_shipping_option_rate_id( $this->flat_rate_id ) ] );
+ $this->system_under_test->update_shipping_method( [ self::get_shipping_option_rate_id( $this->flat_rate_id ) ] );
WC()->cart->calculate_totals();
}
@@ -195,34 +158,34 @@ public function test_common_get_button_settings() {
'height' => '48',
'radius' => '',
],
- $this->mock_express_checkout_helper->get_common_button_settings()
+ $this->system_under_test->get_common_button_settings()
);
}
public function test_cart_prices_include_tax_with_tax_disabled() {
add_filter( 'wc_tax_enabled', '__return_false' );
- $this->assertTrue( $this->mock_express_checkout_helper->cart_prices_include_tax() );
+ $this->assertTrue( $this->system_under_test->cart_prices_include_tax() );
}
public function test_cart_prices_include_tax_with_tax_enabled_and_display_incl() {
add_filter( 'wc_tax_enabled', '__return_true' ); // reset in tear_down.
add_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, '__return_incl' ] ); // reset in tear_down.
- $this->assertTrue( $this->mock_express_checkout_helper->cart_prices_include_tax() );
+ $this->assertTrue( $this->system_under_test->cart_prices_include_tax() );
}
public function test_cart_prices_include_tax_with_tax_enabled_and_display_excl() {
add_filter( 'wc_tax_enabled', '__return_true' ); // reset in tear_down.
add_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, '__return_excl' ] ); // reset in tear_down.
- $this->assertFalse( $this->mock_express_checkout_helper->cart_prices_include_tax() );
+ $this->assertFalse( $this->system_under_test->cart_prices_include_tax() );
}
public function test_get_total_label() {
$this->mock_wcpay_account->method( 'get_statement_descriptor' )
->willReturn( 'Google Pay' );
- $result = $this->mock_express_checkout_helper->get_total_label();
+ $result = $this->system_under_test->get_total_label();
$this->assertEquals( 'Google Pay (via WooCommerce)', $result );
}
@@ -238,49 +201,54 @@ function () {
}
);
- $result = $this->mock_express_checkout_helper->get_total_label();
+ $result = $this->system_under_test->get_total_label();
$this->assertEquals( 'Google Pay (via WooPayments)', $result );
remove_all_filters( 'wcpay_payment_request_total_label_suffix' );
}
- public function test_filter_cart_needs_shipping_address_returns_false() {
- sleep( 1 );
- $this->zone->delete_shipping_method( $this->flat_rate_id );
- $this->zone->delete_shipping_method( $this->local_pickup_id );
+ public function test_should_show_express_checkout_button_for_non_shipping_but_price_includes_tax() {
+ $this->mock_wcpay_account
+ ->method( 'is_stripe_connected' )
+ ->willReturn( true );
- WC_Subscriptions_Cart::set_cart_contains_subscription( true );
+ WC_Payments::mode()->dev();
- $this->mock_ece_button_helper
- ->method( 'is_product' )
- ->willReturn( true );
+ add_filter( 'woocommerce_is_checkout', '__return_true' );
+ add_filter( 'wc_shipping_enabled', '__return_false' );
+ add_filter( 'wc_tax_enabled', '__return_true' );
- $this->mock_express_checkout_ece_button_handler = new WC_Payments_Express_Checkout_Button_Handler(
- $this->mock_wcpay_account,
- $this->mock_wcpay_gateway,
- $this->mock_ece_button_helper,
- $this->mock_express_checkout_ajax_handler
- );
+ update_option( 'woocommerce_tax_based_on', 'billing' );
+ update_option( 'woocommerce_prices_include_tax', 'yes' );
- $this->assertFalse( $this->mock_express_checkout_ece_button_handler->filter_cart_needs_shipping_address( true ) );
+ $this->assertTrue( $this->system_under_test->should_show_express_checkout_button() );
+
+ remove_filter( 'woocommerce_is_checkout', '__return_true' );
+ remove_filter( 'wc_tax_enabled', '__return_true' );
+ remove_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, '__return_incl' ] );
}
- public function test_filter_cart_needs_shipping_address_returns_true() {
- WC_Subscriptions_Cart::set_cart_contains_subscription( true );
- $this->mock_ece_button_helper
- ->method( 'is_product' )
+ public function test_should_not_show_express_checkout_button_for_non_shipping_but_price_does_not_include_tax() {
+ $this->mock_wcpay_account
+ ->method( 'is_stripe_connected' )
->willReturn( true );
- $this->mock_express_checkout_ece_button_handler = new WC_Payments_Express_Checkout_Button_Handler(
- $this->mock_wcpay_account,
- $this->mock_wcpay_gateway,
- $this->mock_ece_button_helper,
- $this->mock_express_checkout_ajax_handler
- );
+ WC_Payments::mode()->dev();
+
+ add_filter( 'woocommerce_is_checkout', '__return_true' );
+ add_filter( 'wc_shipping_enabled', '__return_false' );
+ add_filter( 'wc_tax_enabled', '__return_true' );
+
+ update_option( 'woocommerce_tax_based_on', 'billing' );
+ update_option( 'woocommerce_prices_include_tax', 'no' );
- $this->assertTrue( $this->mock_express_checkout_ece_button_handler->filter_cart_needs_shipping_address( true ) );
+ $this->assertFalse( $this->system_under_test->should_show_express_checkout_button() );
+
+ remove_filter( 'woocommerce_is_checkout', '__return_true' );
+ remove_filter( 'wc_tax_enabled', '__return_true' );
+ remove_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, '__return_incl' ] );
}
/**
diff --git a/tests/unit/multi-currency/compatibility/test-class-woocommerce-fedex.php b/tests/unit/multi-currency/compatibility/test-class-woocommerce-fedex.php
index 60e130390fd..e52927230ca 100644
--- a/tests/unit/multi-currency/compatibility/test-class-woocommerce-fedex.php
+++ b/tests/unit/multi-currency/compatibility/test-class-woocommerce-fedex.php
@@ -35,6 +35,20 @@ class WCPay_Multi_Currency_WooCommerceFedEx_Tests extends WCPAY_UnitTestCase {
*/
private $woocommerce_fedex;
+ /**
+ * Calls to check in the backtrace.
+ *
+ * @var array
+ */
+ private $woocommerce_fedex_calls = [
+ 'WC_Shipping_Fedex->set_settings',
+ 'WC_Shipping_Fedex->per_item_shipping',
+ 'WC_Shipping_Fedex->box_shipping',
+ 'WC_Shipping_Fedex->get_fedex_api_request',
+ 'WC_Shipping_Fedex->get_fedex_requests',
+ 'WC_Shipping_Fedex->process_result',
+ ];
+
/**
* Pre-test setup
*/
@@ -54,37 +68,45 @@ public function test_should_return_store_currency_returns_true_if_true_passed()
// If the calls are found, it should return true.
public function test_should_return_store_currency_returns_true_if_calls_found() {
- $calls = [
- 'WC_Shipping_Fedex->set_settings',
- 'WC_Shipping_Fedex->per_item_shipping',
- 'WC_Shipping_Fedex->box_shipping',
- 'WC_Shipping_Fedex->get_fedex_api_request',
- 'WC_Shipping_Fedex->get_fedex_requests',
- 'WC_Shipping_Fedex->process_result',
- ];
$this->mock_utils
->expects( $this->once() )
->method( 'is_call_in_backtrace' )
- ->with( $calls )
+ ->with( $this->woocommerce_fedex_calls )
->willReturn( true );
+
$this->assertTrue( $this->woocommerce_fedex->should_return_store_currency( false ) );
}
- // If the calls are found, it should return true.
+ // If the calls are not found, it should return false.
public function test_should_return_store_currency_returns_false_if_no_calls_found() {
- $calls = [
- 'WC_Shipping_Fedex->set_settings',
- 'WC_Shipping_Fedex->per_item_shipping',
- 'WC_Shipping_Fedex->box_shipping',
- 'WC_Shipping_Fedex->get_fedex_api_request',
- 'WC_Shipping_Fedex->get_fedex_requests',
- 'WC_Shipping_Fedex->process_result',
- ];
$this->mock_utils
->expects( $this->once() )
->method( 'is_call_in_backtrace' )
- ->with( $calls )
+ ->with( $this->woocommerce_fedex_calls )
->willReturn( false );
+
$this->assertFalse( $this->woocommerce_fedex->should_return_store_currency( false ) );
}
+
+ // If true is passed to should_convert_product_price and no calls are found, it should return true.
+ public function test_should_convert_product_price_returns_true_if_true_passed_and_no_calls_found() {
+ $this->mock_utils
+ ->expects( $this->once() )
+ ->method( 'is_call_in_backtrace' )
+ ->with( $this->woocommerce_fedex_calls )
+ ->willReturn( false );
+
+ $this->assertTrue( $this->woocommerce_fedex->should_convert_product_price( true ) );
+ }
+
+ // If calls are found, should_convert_product_price should return false even if true was passed.
+ public function test_should_convert_product_price_returns_false_if_calls_found() {
+ $this->mock_utils
+ ->expects( $this->once() )
+ ->method( 'is_call_in_backtrace' )
+ ->with( $this->woocommerce_fedex_calls )
+ ->willReturn( true );
+
+ $this->assertFalse( $this->woocommerce_fedex->should_convert_product_price( true ) );
+ }
}
diff --git a/tests/unit/multi-currency/test-class-multi-currency.php b/tests/unit/multi-currency/test-class-multi-currency.php
index 6d4eddaab84..a5a254ed7ec 100644
--- a/tests/unit/multi-currency/test-class-multi-currency.php
+++ b/tests/unit/multi-currency/test-class-multi-currency.php
@@ -620,24 +620,27 @@ public function test_get_price_returns_converted_coupon_price_without_adjustment
WC()->session->set( WCPay\MultiCurrency\MultiCurrency::CURRENCY_SESSION_KEY, 'GBP' );
add_filter( 'wcpay_multi_currency_apply_charm_only_to_products', '__return_false' );
- // 0.708099 * 10 = 7,08099
- $this->assertSame( 7.08099, $this->multi_currency->get_price( '10.0', 'coupon' ) );
+ // 0.708099 * 10 = 7.08099.
+ // ceil( 7.08099, 2 ) = 7.09.
+ $this->assertSame( 7.09, $this->multi_currency->get_price( '10.0', 'coupon' ) );
}
public function test_get_price_returns_converted_exchange_rate_without_adjustments() {
WC()->session->set( WCPay\MultiCurrency\MultiCurrency::CURRENCY_SESSION_KEY, 'GBP' );
add_filter( 'wcpay_multi_currency_apply_charm_only_to_products', '__return_false' );
- // 0.708099 * 10 = 7,08099
- $this->assertSame( 7.08099, $this->multi_currency->get_price( '10.0', 'exchange_rate' ) );
+ // 0.708099 * 10 = 7.08099.
+ // ceil( 7.08099, 2 ) = 7.09.
+ $this->assertSame( 7.09, $this->multi_currency->get_price( '10.0', 'exchange_rate' ) );
}
public function test_get_price_returns_converted_tax_price() {
WC()->session->set( WCPay\MultiCurrency\MultiCurrency::CURRENCY_SESSION_KEY, 'GBP' );
add_filter( 'wcpay_multi_currency_apply_charm_only_to_products', '__return_false' );
- // 0.708099 * 10 = 7,08099
- $this->assertSame( 7.08099, $this->multi_currency->get_price( '10.0', 'tax' ) );
+ // 0.708099 * 10 = 7.08099.
+ // ceil( 7.08099, 2 ) = 7.09.
+ $this->assertSame( 7.09, $this->multi_currency->get_price( '10.0', 'tax' ) );
}
/**
@@ -1014,7 +1017,7 @@ public function test_set_new_customer_currency_meta_does_not_update_user_meta_if
public function get_price_provider() {
return [
- [ '5.2499', '0.00', 5.2499 ],
+ [ '5.2499', '0.00', 5.25 ], // Even though the precision is 0.00 we make sure the amount is ceiled to the currency's number of digits.
[ '5.2499', '0.25', 5.25 ],
[ '5.2500', '0.25', 5.25 ],
[ '5.2501', '0.25', 5.50 ],
diff --git a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php
index 9d6185387f6..a3284a840ce 100644
--- a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php
+++ b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php
@@ -1140,59 +1140,6 @@ public function test_get_payment_methods_without_request_context() {
$this->assertSame( [ Payment_Method::CARD ], $payment_methods );
}
- /**
- * Test get_payment_method_types without post request context or saved token.
- *
- * @return void
- */
- public function test_get_payment_methods_without_request_context_or_token() {
- $mock_upe_gateway = $this->getMockBuilder( WC_Payment_Gateway_WCPay::class )
- ->setConstructorArgs(
- [
- $this->mock_api_client,
- $this->mock_wcpay_account,
- $this->mock_customer_service,
- $this->mock_token_service,
- $this->mock_action_scheduler_service,
- $this->mock_payment_methods[ Payment_Method::CARD ],
- $this->mock_payment_methods,
- $this->order_service,
- $this->mock_dpps,
- $this->mock_localization_service,
- $this->mock_fraud_service,
- $this->mock_duplicates_detection_service,
- $this->mock_rate_limiter,
- ]
- )
- ->setMethods(
- [
- 'get_payment_methods_from_gateway_id',
- 'get_payment_method_ids_enabled_at_checkout',
- ]
- )
- ->getMock();
-
- $payment_information = new Payment_Information( 'pm_mock' );
-
- unset( $_POST['payment_method'] ); // phpcs:ignore WordPress.Security.NonceVerification
-
- $gateway = WC_Payments::get_gateway();
- WC_Payments::set_gateway( $mock_upe_gateway );
-
- $mock_upe_gateway->expects( $this->never() )
- ->method( 'get_payment_methods_from_gateway_id' );
-
- $mock_upe_gateway->expects( $this->once() )
- ->method( 'get_payment_method_ids_enabled_at_checkout' )
- ->willReturn( [ Payment_Method::CARD ] );
-
- $payment_methods = $mock_upe_gateway->get_payment_method_types( $payment_information );
-
- $this->assertSame( [ Payment_Method::CARD ], $payment_methods );
-
- WC_Payments::set_gateway( $gateway );
- }
-
/**
* Test get_payment_methods_from_gateway_id function with UPE enabled.
*
diff --git a/tests/unit/src/Internal/Service/Level3ServiceTest.php b/tests/unit/src/Internal/Service/Level3ServiceTest.php
index fe3eee573c3..dba9766386d 100644
--- a/tests/unit/src/Internal/Service/Level3ServiceTest.php
+++ b/tests/unit/src/Internal/Service/Level3ServiceTest.php
@@ -167,6 +167,10 @@ protected function mock_level_3_order(
$mock_items = array_merge( $mock_items, array_fill( 0, $basket_size - count( $mock_items ), $mock_items[0] ) );
}
+ $this->mock_order( $mock_items, $shipping_postcode );
+ }
+
+ protected function mock_order( array $mock_items, string $shipping_postcode ) {
// Setup the order.
$mock_order = $this
->getMockBuilder( WC_Order::class )
@@ -434,6 +438,25 @@ public function test_full_level3_data_with_float_quantity() {
$this->assertEquals( $expected_data, $level_3_data );
}
+ public function test_rounding_in_edge_cases() {
+ $this->mock_account->method( 'get_account_country' )->willReturn( Country_Code::UNITED_STATES );
+
+ $mock_items = [];
+ $mock_items[] = $this->create_mock_item( 'Beanie with Addon', 3, 73, 0, 30 );
+ $this->mock_order( $mock_items, '98012' );
+
+ $level_3_data = $this->sut->get_data_from_order( $this->order_id );
+
+ $this->assertCount( 2, $level_3_data['line_items'] );
+ $this->assertEquals( 2433, $level_3_data['line_items'][0]->unit_cost );
+ $this->assertEquals( 'rounding-fix', $level_3_data['line_items'][1]->product_code );
+ $this->assertEquals( 'Rounding fix', $level_3_data['line_items'][1]->product_description );
+ $this->assertEquals( 1, $level_3_data['line_items'][1]->unit_cost );
+ $this->assertEquals( 1, $level_3_data['line_items'][1]->quantity );
+ $this->assertEquals( 0, $level_3_data['line_items'][1]->tax_amount );
+ $this->assertEquals( 0, $level_3_data['line_items'][1]->discount_amount );
+ }
+
public function test_full_level3_data_with_float_quantity_zero() {
$expected_data = [
'merchant_reference' => '210',
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php
index 4b7bf857997..1827041a1fc 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php
@@ -1239,21 +1239,6 @@ public function test_get_payment_methods_without_post_request_context() {
$this->assertSame( [ Payment_Method::CARD ], $payment_methods );
}
- public function test_get_payment_methods_without_request_context_or_token() {
- $payment_information = new Payment_Information( 'pm_mock' );
-
- unset( $_POST['payment_method'] ); // phpcs:ignore WordPress.Security.NonceVerification
-
- $gateway = WC_Payments::get_gateway();
- WC_Payments::set_gateway( $this->card_gateway );
-
- $payment_methods = $this->card_gateway->get_payment_method_types( $payment_information );
-
- $this->assertSame( [ Payment_Method::CARD ], $payment_methods );
-
- WC_Payments::set_gateway( $gateway );
- }
-
public function test_get_payment_methods_from_gateway_id_upe() {
WC_Helper_Order::create_order();
@@ -2542,7 +2527,7 @@ public function test_process_payment_for_order_not_from_request() {
$order->add_payment_token( $token );
$order->save();
- $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' );
+ $pi = new Payment_Information( 'pm_test', $order, null, $token, null, null, null, '', 'card' );
$request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class );
$request->expects( $this->once() )
@@ -3086,7 +3071,10 @@ public function test_process_payment_caches_mimimum_amount_and_displays_error_up
->method( 'get_customer_id_by_user_id' )
->will( $this->returnValue( $customer ) );
- $_POST = [ 'wcpay-payment-method' => $pm = 'pm_mock' ];
+ $_POST = [
+ 'wcpay-payment-method' => $pm = 'pm_mock',
+ 'payment_method' => 'woocommerce_payments',
+ ];
$this->get_fraud_prevention_service_mock()
->expects( $this->once() )
@@ -3922,7 +3910,10 @@ public function test_process_payment_rate_limiter_enabled_throw_exception() {
public function test_process_payment_returns_correct_redirect() {
$order = WC_Helper_Order::create_order();
- $_POST = [ 'wcpay-payment-method' => 'pm_mock' ];
+ $_POST = [
+ 'wcpay-payment-method' => 'pm_mock',
+ 'payment_method' => 'woocommerce_payments',
+ ];
$this->mock_wcpay_request( Create_And_Confirm_Intention::class, 1 )
->expects( $this->once() )
@@ -3945,7 +3936,10 @@ public function test_process_payment_returns_correct_redirect() {
public function test_process_payment_returns_correct_redirect_when_using_payment_request() {
$order = WC_Helper_Order::create_order();
$_POST['payment_request_type'] = 'google_pay';
- $_POST = [ 'wcpay-payment-method' => 'pm_mock' ];
+ $_POST = [
+ 'wcpay-payment-method' => 'pm_mock',
+ 'payment_method' => 'woocommerce_payments',
+ ];
$this->mock_wcpay_request( Create_And_Confirm_Intention::class, 1 )
->expects( $this->once() )
@@ -3969,6 +3963,57 @@ public function is_proper_intent_used_with_order_returns_false() {
$this->assertFalse( $this->card_gateway->is_proper_intent_used_with_order( WC_Helper_Order::create_order(), 'wrong_intent_id' ) );
}
+ public function test_get_recommended_payment_method() {
+ $this->mock_wcpay_account
+ ->expects( $this->once() )
+ ->method( 'get_recommended_payment_methods' )
+ ->with( 'US' );
+ $this->card_gateway->get_recommended_payment_methods( 'US' );
+ }
+
+ public function get_recommended_payment_method_no_country_code_provider() {
+ return [
+ 'provider connected' => [ true, 'test' ],
+ 'provider not connected' => [ false, 'US' ],
+ ];
+ }
+
+ /**
+ * @dataProvider get_recommended_payment_method_no_country_code_provider
+ */
+ public function test_get_recommended_payment_method_no_country_code_provided( $is_provider_connected, $country_code ) {
+ // Set base country fallback to US.
+ $filter_callback = function () {
+ return 'US';
+ };
+ add_filter( 'woocommerce_countries_base_country', $filter_callback );
+
+ $this->mock_wcpay_account
+ ->expects( $this->once() )
+ ->method( 'is_provider_connected' )
+ ->willReturn( $is_provider_connected );
+
+ $this->mock_wcpay_account
+ ->expects( $this->any() )
+ ->method( 'is_stripe_connected' )
+ ->willReturn( true );
+
+ $this->mock_wcpay_account
+ ->expects( $this->any() )
+ ->method( 'get_account_country' )
+ ->willReturn( $country_code );
+
+ $this->mock_wcpay_account
+ ->expects( $this->once() )
+ ->method( 'get_recommended_payment_methods' )
+ ->with( $country_code );
+
+ $this->assertSame( [], $this->card_gateway->get_recommended_payment_methods( '' ) );
+
+ // Clean up.
+ remove_filter( 'woocommerce_countries_base_country', $filter_callback );
+ }
+
/**
* Sets up the expectation for a certain factor for the new payment
* process to be either set or unset.
diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php
index f0087e3e966..d46a32722af 100644
--- a/tests/unit/test-class-wc-payments-account.php
+++ b/tests/unit/test-class-wc-payments-account.php
@@ -907,6 +907,76 @@ public function test_maybe_handle_onboarding_init_embedded_kyc() {
$this->wcpay_account->maybe_handle_onboarding();
}
+ public function test_ensure_woopay_enabled_by_default_value_set_in_sandbox_mode_kyc() {
+ // Arrange.
+ // We need to be in the WP admin dashboard.
+ $this->set_is_admin( true );
+ // Test as an admin user.
+ wp_set_current_user( 1 );
+
+ // Configure the request to be in sandbox mode.
+ $_GET['wcpay-connect'] = 'connect-from';
+ $_REQUEST['_wpnonce'] = wp_create_nonce( 'wcpay-connect' );
+ $_GET['progressive'] = 'true';
+ $_GET['test_mode'] = 'true';
+ $_GET['from'] = WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD;
+
+ // The Jetpack connection is in working order.
+ $this->mock_jetpack_connection();
+
+ $this->mock_api_client
+ ->expects( $this->once() )
+ ->method( 'get_onboarding_data' )
+ ->willReturn(
+ [
+ 'url' => false,
+ 'woopay_enabled_by_default' => true,
+ ]
+ );
+
+ $original_value = get_transient( WC_Payments_Account::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT );
+
+ // Act.
+ $this->wcpay_account->maybe_handle_onboarding();
+
+ // Assert.
+ $this->assertFalse( $original_value );
+ $this->assertTrue( get_transient( WC_Payments_Account::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT ) );
+ }
+
+ public function test_ensure_woopay_not_enabled_by_default_for_existing_live_accounts() {
+ // Arrange.
+ // We need to be in the WP admin dashboard.
+ $this->set_is_admin( true );
+ // Test as an admin user.
+ wp_set_current_user( 1 );
+
+ // Configure the request to be in sandbox mode.
+ $_GET['wcpay-connect'] = 'connect-from';
+ $_REQUEST['_wpnonce'] = wp_create_nonce( 'wcpay-connect' );
+ $_GET['progressive'] = 'true';
+ $_GET['from'] = WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD;
+
+ // The Jetpack connection is in working order.
+ $this->mock_jetpack_connection();
+
+ $this->mock_api_client
+ ->expects( $this->once() )
+ ->method( 'get_onboarding_data' )
+ ->willReturn(
+ [
+ 'url' => false,
+ 'woopay_enabled_by_default' => true,
+ ]
+ );
+
+ // Act.
+ $this->wcpay_account->maybe_handle_onboarding();
+
+ // Assert.
+ $this->assertFalse( get_transient( WC_Payments_Account::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT ) );
+ }
+
public function test_maybe_handle_onboarding_init_stripe_onboarding_existing_account() {
// Arrange.
// We need to be in the WP admin dashboard.
@@ -3174,6 +3244,91 @@ public function test_get_tracking_info() {
$this->assertSame( $expected, $this->wcpay_account->get_tracking_info() );
}
+ public function test_get_recommended_payment_methods_unsupported_country() {
+ $this->assertSame( [], $this->wcpay_account->get_recommended_payment_methods( 'XZ' ) );
+ }
+
+ public function get_recommended_payment_methods_provider() {
+ return [
+ 'No PMs suggested' => [ 'US', [], [] ],
+ 'Invalid PMs array' => [
+ 'US',
+ [
+ 'type' => 'available',
+ 'enabled' => false,
+ ],
+ [],
+ ],
+ 'Enabled flag and priority not set' => [
+ 'US',
+ [
+ [
+ 'id' => 1,
+ 'title' => 'test PM',
+ 'type' => 'available',
+ ],
+ [
+ 'id' => 2,
+ 'title' => 'test PM 2',
+ 'type' => 'available',
+ ],
+ ],
+ [
+ [
+ 'id' => 1,
+ 'title' => 'test PM',
+ 'type' => 'available',
+ 'enabled' => false,
+ 'priority' => 0,
+ ],
+ [
+ 'id' => 2,
+ 'title' => 'test PM 2',
+ 'type' => 'available',
+ 'enabled' => false,
+ 'priority' => 1,
+ ],
+ ],
+ ],
+ 'Enabled flag and priority set' => [
+ 'US',
+ [
+ [
+ 'id' => 1,
+ 'title' => 'test PM',
+ 'type' => 'available',
+ 'enabled' => true,
+ 'priority' => 1,
+ ],
+ ],
+ [
+ [
+ 'id' => 1,
+ 'title' => 'test PM',
+ 'type' => 'available',
+ 'enabled' => true,
+ 'priority' => 1,
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider get_recommended_payment_methods_provider
+ */
+ public function test_get_recommended_payment_methods( $country_code, $recommended_pms, $expected ) {
+
+ $this->mock_empty_cache();
+ $this->mock_onboarding_service
+ ->expects( $this->once() )
+ ->method( 'get_recommended_payment_methods' )
+ ->with( $country_code )
+ ->willReturn( $recommended_pms );
+
+ $this->assertSame( $expected, $this->wcpay_account->get_recommended_payment_methods( $country_code ) );
+ }
+
/**
* Sets up the mocked cache to simulate that its empty and call the generator.
*/
diff --git a/tests/unit/test-class-wc-payments-order-service.php b/tests/unit/test-class-wc-payments-order-service.php
index 2eeaa50864e..d3fef27ea1f 100644
--- a/tests/unit/test-class-wc-payments-order-service.php
+++ b/tests/unit/test-class-wc-payments-order-service.php
@@ -1382,4 +1382,33 @@ public function test_add_note_and_metadata_for_refund_partially_refunded(): void
WC_Helper_Order::delete_order( $order->get_id() );
}
+
+ public function test_process_captured_payment() {
+ $order = WC_Helper_Order::create_order();
+ $order->save();
+
+ $intent = WC_Helper_Intention::create_intention( [ 'status' => Intent_Status::SUCCEEDED ] );
+ $this->order_service->set_intention_status_for_order( $this->order, Intent_Status::REQUIRES_CAPTURE );
+ $this->order_service->set_intent_id_for_order( $order, $intent->get_id() );
+ $order->set_status( Order_Status::PROCESSING ); // Let's simulate that order is set to processing, so order status should not interfere with the process.
+ $order->save();
+
+ $this->order_service->process_captured_payment( $order, $intent );
+
+ $this->assertEquals( $intent->get_status(), $this->order_service->get_intention_status_for_order( $order ) );
+
+ $this->assertTrue( $order->has_status( wc_get_is_paid_statuses() ) );
+
+ $notes = wc_get_order_notes( [ 'order_id' => $order->get_id() ] );
+ $this->assertStringContainsString( 'successfully captured using WooPayments', $notes[0]->content );
+ $this->assertStringContainsString( '/payments/transactions/details&id=pi_mock" target="_blank" rel="noopener noreferrer">pi_mock', $notes[0]->content );
+
+ // Assert: Check that the order was unlocked.
+ $this->assertFalse( get_transient( 'wcpay_processing_intent_' . $order->get_id() ) );
+
+ // Assert: Applying the same data multiple times does not cause duplicate actions.
+ $this->order_service->update_order_status_from_intent( $order, $intent );
+ $notes_2 = wc_get_order_notes( [ 'order_id' => $order->get_id() ] );
+ $this->assertEquals( count( $notes ), count( $notes_2 ) );
+ }
}
diff --git a/tests/unit/test-class-wc-payments-payment-request-button-handler.php b/tests/unit/test-class-wc-payments-payment-request-button-handler.php
deleted file mode 100644
index b34299b76f6..00000000000
--- a/tests/unit/test-class-wc-payments-payment-request-button-handler.php
+++ /dev/null
@@ -1,650 +0,0 @@
- Country_Code::UNITED_STATES,
- 'state' => 'CA',
- 'postcode' => '94110',
- 'city' => 'San Francisco',
- 'address_1' => '60 29th Street',
- 'address_2' => '#343',
- ];
-
- /**
- * Mock WC_Payments_API_Client.
- *
- * @var WC_Payments_API_Client
- */
- private $mock_api_client;
-
- /**
- * Payment request instance.
- *
- * @var WC_Payments_Payment_Request_Button_Handler
- */
- private $pr;
-
- /**
- * WC_Payments_Account instance.
- *
- * @var WC_Payments_Account
- */
- private $mock_wcpay_account;
-
- /**
- * Test product to add to the cart
- * @var WC_Product_Simple
- */
- private $simple_product;
-
- /**
- * Test shipping zone.
- *
- * @var WC_Shipping_Zone
- */
- private $zone;
-
- /**
- * Flat rate shipping method instance id
- *
- * @var int
- */
- private $flat_rate_id;
-
- /**
- * Flat rate shipping method instance id
- *
- * @var int
- */
- private $local_pickup_id;
-
- /**
- * Used to get the settings.
- *
- * @var WC_Payment_Gateway_WCPay
- */
- private $mock_wcpay_gateway;
-
- /**
- * Express Checkout Helper instance.
- *
- * @var WC_Payments_Express_Checkout_Button_Helper
- */
- private $express_checkout_helper;
-
- /**
- * Sets up things all tests need.
- */
- public function set_up() {
- parent::set_up();
- add_filter( 'pre_option_woocommerce_tax_based_on', [ $this, '__return_base' ] );
-
- $this->mock_api_client = $this->getMockBuilder( 'WC_Payments_API_Client' )
- ->disableOriginalConstructor()
- ->setMethods(
- [
- 'get_account_data',
- 'is_server_connected',
- 'capture_intention',
- 'cancel_intention',
- 'get_payment_method',
- ]
- )
- ->getMock();
- $this->mock_api_client->expects( $this->any() )->method( 'is_server_connected' )->willReturn( true );
- $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
-
- $this->mock_wcpay_gateway = $this->make_wcpay_gateway();
-
- $this->express_checkout_helper = $this->getMockBuilder( WC_Payments_Express_Checkout_Button_Helper::class )
- ->setMethods(
- [
- 'is_product',
- 'get_product',
- ]
- )
- ->setConstructorArgs( [ $this->mock_wcpay_gateway, $this->mock_wcpay_account ] )
- ->getMock();
-
- $this->pr = new WC_Payments_Payment_Request_Button_Handler( $this->mock_wcpay_account, $this->mock_wcpay_gateway, $this->express_checkout_helper );
-
- $this->simple_product = WC_Helper_Product::create_simple_product();
-
- WC_Helper_Shipping::delete_simple_flat_rate();
- $zone = new WC_Shipping_Zone();
- $zone->set_zone_name( 'Worldwide' );
- $zone->set_zone_order( 1 );
- $zone->save();
-
- add_filter(
- 'woocommerce_find_rates',
- function () {
- return [
- 1 =>
- [
- 'rate' => 10.0,
- 'label' => 'Tax',
- 'shipping' => 'yes',
- 'compound' => 'no',
- ],
- ];
- },
- 50,
- 2
- );
-
- $this->flat_rate_id = $zone->add_shipping_method( 'flat_rate' );
- self::set_shipping_method_cost( $this->flat_rate_id, '5' );
-
- $this->local_pickup_id = $zone->add_shipping_method( 'local_pickup' );
- self::set_shipping_method_cost( $this->local_pickup_id, '1' );
-
- $this->zone = $zone;
-
- WC()->session->init();
- WC()->cart->add_to_cart( $this->simple_product->get_id(), 1 );
- WC()->cart->calculate_totals();
- }
-
- public function tear_down() {
- WC_Subscriptions_Cart::set_cart_contains_subscription( false );
- WC()->cart->empty_cart();
- WC()->session->cleanup_sessions();
- $this->zone->delete();
- delete_option( 'woocommerce_woocommerce_payments_settings' );
- remove_filter( 'pre_option_woocommerce_tax_based_on', [ $this, '__return_base' ] );
- remove_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, '__return_excl' ] );
- remove_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, '__return_incl' ] );
- remove_filter( 'pre_option_woocommerce_tax_display_shop', [ $this, '__return_excl' ] );
- remove_filter( 'pre_option_woocommerce_tax_display_shop', [ $this, '__return_incl' ] );
- remove_filter( 'pre_option_woocommerce_prices_include_tax', [ $this, '__return_yes' ] );
- remove_filter( 'pre_option_woocommerce_prices_include_tax', [ $this, '__return_no' ] );
- remove_filter( 'wc_tax_enabled', '__return_true' );
- remove_filter( 'wc_tax_enabled', '__return_false' );
- remove_filter( 'wc_shipping_enabled', '__return_false' );
- remove_all_filters( 'woocommerce_find_rates' );
-
- parent::tear_down();
- }
-
- public function __return_yes() {
- return 'yes';
- }
-
- public function __return_no() {
- return 'no';
- }
-
- public function __return_excl() {
- return 'excl';
- }
-
- public function __return_incl() {
- return 'incl';
- }
-
- public function __return_base() {
- return 'base';
- }
-
- /**
- * @return WC_Payment_Gateway_WCPay
- */
- private function make_wcpay_gateway() {
- $mock_customer_service = $this->createMock( WC_Payments_Customer_Service::class );
- $mock_token_service = $this->createMock( WC_Payments_Token_Service::class );
- $mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class );
- $mock_rate_limiter = $this->createMock( Session_Rate_Limiter::class );
- $mock_order_service = $this->createMock( WC_Payments_Order_Service::class );
- $mock_dpps = $this->createMock( Duplicate_Payment_Prevention_Service::class );
- $mock_payment_method = $this->createMock( CC_Payment_Method::class );
-
- return new WC_Payment_Gateway_WCPay(
- $this->mock_api_client,
- $this->mock_wcpay_account,
- $mock_customer_service,
- $mock_token_service,
- $mock_action_scheduler_service,
- $mock_payment_method,
- [ 'card' => $mock_payment_method ],
- $mock_order_service,
- $mock_dpps,
- $this->createMock( WC_Payments_Localization_Service::class ),
- $this->createMock( WC_Payments_Fraud_Service::class ),
- $this->createMock( Duplicates_Detection_Service::class ),
- $mock_rate_limiter
- );
- }
-
- /**
- * Sets shipping method cost
- *
- * @param string $instance_id Shipping method instance id
- * @param string $cost Shipping method cost in USD
- */
- private static function set_shipping_method_cost( $instance_id, $cost ) {
- $method = WC_Shipping_Zones::get_shipping_method( $instance_id );
- $option_key = $method->get_instance_option_key();
- $options = get_option( $option_key );
- $options['cost'] = $cost;
- update_option( $option_key, $options );
- }
-
- /**
- * Retrieves rate id by shipping method instance id.
- *
- * @param string $instance_id Shipping method instance id.
- *
- * @return string Shipping option instance rate id.
- */
- private static function get_shipping_option_rate_id( $instance_id ) {
- $method = WC_Shipping_Zones::get_shipping_method( $instance_id );
-
- return $method->get_rate_id();
- }
-
- public function test_multiple_packages_in_cart_not_allowed() {
- // Add fake packages to the cart.
- add_filter(
- 'woocommerce_cart_shipping_packages',
- function () {
- return [
- 'fake_package_1',
- 'fake_package_2',
- ];
- }
- );
- $this->mock_wcpay_gateway = $this->make_wcpay_gateway();
- $this->pr = new WC_Payments_Payment_Request_Button_Handler( $this->mock_wcpay_account, $this->mock_wcpay_gateway, $this->express_checkout_helper );
-
- $this->assertFalse( $this->pr->has_allowed_items_in_cart() );
- }
-
- public function test_get_product_price_returns_simple_price() {
- $this->assertEquals(
- $this->simple_product->get_price(),
- $this->pr->get_product_price( $this->simple_product )
- );
- }
-
- public function test_get_product_price_returns_deposit_amount() {
- $product_price = 10;
- $this->simple_product->set_price( $product_price );
-
- $this->assertEquals(
- $product_price,
- $this->pr->get_product_price( $this->simple_product, false ),
- 'When deposit is disabled, the regular price should be returned.'
- );
- $this->assertEquals(
- $product_price,
- $this->pr->get_product_price( $this->simple_product, true ),
- 'When deposit is enabled, but the product has no setting for deposit, the regular price should be returned.'
- );
-
- $this->simple_product->update_meta_data( '_wc_deposit_enabled', 'optional' );
- $this->simple_product->update_meta_data( '_wc_deposit_type', 'percent' );
- $this->simple_product->update_meta_data( '_wc_deposit_amount', 50 );
- $this->simple_product->save_meta_data();
-
- $this->assertEquals(
- $product_price,
- $this->pr->get_product_price( $this->simple_product, false ),
- 'When deposit is disabled, the regular price should be returned.'
- );
- $this->assertEquals(
- $product_price * 0.5,
- $this->pr->get_product_price( $this->simple_product, true ),
- 'When deposit is enabled, the deposit price should be returned.'
- );
-
- $this->simple_product->delete_meta_data( '_wc_deposit_amount' );
- $this->simple_product->delete_meta_data( '_wc_deposit_type' );
- $this->simple_product->delete_meta_data( '_wc_deposit_enabled' );
- $this->simple_product->save_meta_data();
- }
-
- public function test_get_product_price_returns_deposit_amount_default_values() {
- $product_price = 10;
- $this->simple_product->set_price( $product_price );
-
- $this->assertEquals(
- $product_price,
- $this->pr->get_product_price( $this->simple_product ),
- 'When deposit is disabled by default, the regular price should be returned.'
- );
-
- $this->simple_product->update_meta_data( '_wc_deposit_enabled', 'optional' );
- $this->simple_product->update_meta_data( '_wc_deposit_type', 'percent' );
- $this->simple_product->update_meta_data( '_wc_deposit_amount', 50 );
- $this->simple_product->update_meta_data( '_wc_deposit_selected_type', 'full' );
- $this->simple_product->save_meta_data();
-
- $this->assertEquals(
- $product_price,
- $this->pr->get_product_price( $this->simple_product ),
- 'When deposit is optional and disabled by default, the regular price should be returned.'
- );
-
- $this->simple_product->update_meta_data( '_wc_deposit_selected_type', 'deposit' );
- $this->simple_product->save_meta_data();
-
- $this->assertEquals(
- $product_price * 0.5,
- $this->pr->get_product_price( $this->simple_product ),
- 'When deposit is optional and selected by default, the deposit price should be returned.'
- );
- }
-
- /**
- * @dataProvider provide_get_product_tax_tests
- */
- public function test_get_product_price_returns_price_with_tax( $tax_enabled, $prices_include_tax, $tax_display_shop, $tax_display_cart, $product_price, $expected_price ) {
- $this->simple_product->set_price( $product_price );
- add_filter( 'wc_tax_enabled', $tax_enabled ? '__return_true' : '__return_false' ); // reset in tear_down.
- add_filter( 'pre_option_woocommerce_prices_include_tax', [ $this, "__return_$prices_include_tax" ] ); // reset in tear_down.
- add_filter( 'pre_option_woocommerce_tax_display_shop', [ $this, "__return_$tax_display_shop" ] ); // reset in tear_down.
- add_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, "__return_$tax_display_cart" ] ); // reset in tear_down.
- WC()->cart->calculate_totals();
- $this->assertEquals(
- $expected_price,
- $this->pr->get_product_price( $this->simple_product )
- );
- }
-
- public function provide_get_product_tax_tests() {
- yield 'Tax Disabled, Price Display Unaffected' => [
- 'tax_enabled' => false,
- 'prices_include_tax' => 'no',
- 'tax_display_shop' => 'excl',
- 'tax_display_cart' => 'incl',
- 'product_price' => 10,
- 'expected_price' => 10,
- ];
-
- // base prices DO NOT include tax.
-
- yield 'Shop: Excl / Cart: Incl, Base Prices Don\'t Include Tax' => [
- 'tax_enabled' => true,
- 'prices_include_tax' => 'no',
- 'tax_display_shop' => 'excl',
- 'tax_display_cart' => 'incl',
- 'product_price' => 10,
- 'expected_price' => 11,
- ];
- yield 'Shop: Excl / Cart: Excl, Base Prices Don\'t Include Tax' => [
- 'tax_enabled' => true,
- 'prices_include_tax' => 'no',
- 'tax_display_shop' => 'excl',
- 'tax_display_cart' => 'excl',
- 'product_price' => 10,
- 'expected_price' => 10,
- ];
-
- yield 'Shop: Incl / Cart: Incl, Base Prices Don\'t Include Tax' => [
- 'tax_enabled' => true,
- 'prices_include_tax' => 'no',
- 'tax_display_shop' => 'incl',
- 'tax_display_cart' => 'incl',
- 'product_price' => 10,
- 'expected_price' => 11,
- ];
- yield 'Shop: Incl / Cart: Excl, Base Prices Don\'t Include Tax' => [
- 'tax_enabled' => true,
- 'prices_include_tax' => 'no',
- 'tax_display_shop' => 'incl',
- 'tax_display_cart' => 'excl',
- 'product_price' => 10,
- 'expected_price' => 10,
- ];
-
- // base prices include tax.
-
- yield 'Shop: Excl / Cart: Incl, Base Prices Include Tax' => [
- 'tax_enabled' => true,
- 'prices_include_tax' => 'yes',
- 'tax_display_shop' => 'excl',
- 'tax_display_cart' => 'incl',
- 'product_price' => 10,
- 'expected_price' => 10,
- ];
- yield 'Shop: Excl / Cart: Excl, Base Prices Include Tax' => [
- 'tax_enabled' => true,
- 'prices_include_tax' => 'yes',
- 'tax_display_shop' => 'excl',
- 'tax_display_cart' => 'excl',
- 'product_price' => 10,
- 'expected_price' => 9.090909,
- ];
-
- yield 'Shop: Incl / Cart: Incl, Base Prices Include Tax' => [
- 'tax_enabled' => true,
- 'prices_include_tax' => 'yes',
- 'tax_display_shop' => 'incl',
- 'tax_display_cart' => 'incl',
- 'product_price' => 10,
- 'expected_price' => 10,
- ];
- yield 'Shop: Incl / Cart: Excl, Base Prices Include Tax' => [
- 'tax_enabled' => true,
- 'prices_include_tax' => 'yes',
- 'tax_display_shop' => 'incl',
- 'tax_display_cart' => 'excl',
- 'product_price' => 10,
- 'expected_price' => 9.090909,
- ];
- }
-
- public function test_get_product_price_includes_subscription_sign_up_fee() {
- $mock_product = $this->create_mock_subscription( 'subscription' );
- add_filter(
- 'test_deposit_get_product',
- function () use ( $mock_product ) {
- return $mock_product;
- }
- );
-
- // We have a helper because we are not loading subscriptions.
- WC_Subscriptions_Product::set_sign_up_fee( 10 );
-
- $this->assertEquals( 20, $this->pr->get_product_price( $mock_product ) );
-
- // Restore the sign-up fee after the test.
- WC_Subscriptions_Product::set_sign_up_fee( 0 );
-
- remove_all_filters( 'test_deposit_get_product' );
- }
-
- public function test_get_product_price_includes_variable_subscription_sign_up_fee() {
- $mock_product = $this->create_mock_subscription( 'subscription_variation' );
- add_filter(
- 'test_deposit_get_product',
- function () use ( $mock_product ) {
- return $mock_product;
- }
- );
-
- // We have a helper because we are not loading subscriptions.
- WC_Subscriptions_Product::set_sign_up_fee( 10 );
-
- $this->assertEquals( 20, $this->pr->get_product_price( $mock_product ) );
-
- // Restore the sign-up fee after the test.
- WC_Subscriptions_Product::set_sign_up_fee( 0 );
-
- remove_all_filters( 'test_deposit_get_product' );
- }
-
- public function test_get_product_price_throws_exception_for_products_without_prices() {
- if ( version_compare( WC_VERSION, '6.9.0', '>=' ) ) {
- $this->markTestSkipped( 'This test is useless starting with WooCommerce 6.9.0' );
- return;
- }
-
- $this->simple_product->set_price( 'a' );
-
- $this->expectException( WCPay\Exceptions\Invalid_Price_Exception::class );
-
- $this->pr->get_product_price( $this->simple_product );
- }
-
- public function test_get_product_price_throws_exception_for_a_non_numeric_signup_fee() {
- $mock_product = $this->create_mock_subscription( 'subscription' );
- add_filter(
- 'test_deposit_get_product',
- function () use ( $mock_product ) {
- return $mock_product;
- }
- );
- WC_Subscriptions_Product::set_sign_up_fee( 'a' );
-
- $this->expectException( WCPay\Exceptions\Invalid_Price_Exception::class );
- $this->pr->get_product_price( $mock_product );
-
- // Restore the sign-up fee after the test.
- WC_Subscriptions_Product::set_sign_up_fee( 0 );
-
- remove_all_filters( 'test_deposit_get_product' );
- }
-
- private function create_mock_subscription( $type ) {
- $mock_product = $this->createMock( WC_Subscriptions_Product::class );
-
- $mock_product
- ->expects( $this->once() )
- ->method( 'get_price' )
- ->willReturn( 10 );
-
- $mock_product
- ->expects( $this->once() )
- ->method( 'get_type' )
- ->willReturn( $type );
-
- return $mock_product;
- }
-
- /**
- * @dataProvider provide_get_product_tax_tests
- */
- public function test_get_product_data_returns_the_same_as_build_display_items_without_shipping( $tax_enabled, $prices_include_tax, $tax_display_shop, $tax_display_cart, $_product_price, $_expected_price ) {
- add_filter( 'wc_tax_enabled', $tax_enabled ? '__return_true' : '__return_false' ); // reset in tear_down.
- add_filter( 'pre_option_woocommerce_prices_include_tax', [ $this, "__return_$prices_include_tax" ] ); // reset in tear_down.
- add_filter( 'pre_option_woocommerce_tax_display_shop', [ $this, "__return_$tax_display_shop" ] ); // reset in tear_down.
- add_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, "__return_$tax_display_cart" ] ); // reset in tear_down.
- add_filter( 'wc_shipping_enabled', '__return_false' ); // reset in tear_down.
- WC()->cart->calculate_totals();
- $build_display_items_result = $this->express_checkout_helper->build_display_items( true );
-
- $this->express_checkout_helper
- ->method( 'is_product' )
- ->willReturn( true );
-
- $this->express_checkout_helper
- ->method( 'get_product' )
- ->willReturn( $this->simple_product );
-
- $get_product_data_result = $this->pr->get_product_data();
-
- foreach ( $get_product_data_result['displayItems'] as $key => $display_item ) {
- if ( isset( $display_item['pending'] ) ) {
- unset( $get_product_data_result['displayItems'][ $key ]['pending'] );
- }
- }
-
- $this->assertEquals(
- $get_product_data_result['displayItems'],
- $build_display_items_result['displayItems'],
- 'Failed asserting displayItems are the same for get_product_data and build_display_items'
- );
- $this->assertEquals(
- $get_product_data_result['total']['amount'],
- $build_display_items_result['total']['amount'],
- 'Failed asserting total amount are the same for get_product_data and build_display_items'
- );
- }
-
- public function test_filter_cart_needs_shipping_address_returns_false() {
- sleep( 1 );
- $this->zone->delete_shipping_method( $this->flat_rate_id );
- $this->zone->delete_shipping_method( $this->local_pickup_id );
-
- WC_Subscriptions_Cart::set_cart_contains_subscription( true );
-
- $this->assertFalse( $this->pr->filter_cart_needs_shipping_address( true ) );
- }
-
- public function test_filter_cart_needs_shipping_address_returns_true() {
- WC_Subscriptions_Cart::set_cart_contains_subscription( true );
-
- $this->assertTrue( $this->pr->filter_cart_needs_shipping_address( true ) );
- }
-
- public function test_get_button_settings() {
- $this->express_checkout_helper
- ->method( 'is_product' )
- ->willReturn( true );
-
- $this->assertEquals(
- [
- 'type' => 'default',
- 'theme' => 'dark',
- 'height' => '48',
- 'locale' => 'en',
- 'branded_type' => 'short',
- 'radius' => '',
- ],
- $this->pr->get_button_settings()
- );
- }
-
- public function test_filter_gateway_title() {
- $order = $this->createMock( WC_Order::class );
- $order->method( 'get_payment_method_title' )->willReturn( 'Apple Pay' );
-
- global $theorder;
- $theorder = $order;
-
- $this->set_is_admin( true );
- $this->assertEquals( 'Apple Pay', $this->pr->filter_gateway_title( 'Original Title', 'woocommerce_payments' ) );
-
- $this->set_is_admin( false );
- $this->assertEquals( 'Original Title', $this->pr->filter_gateway_title( 'Original Title', 'woocommerce_payments' ) );
-
- $this->set_is_admin( true );
- $this->assertEquals( 'Original Title', $this->pr->filter_gateway_title( 'Original Title', 'another_gateway' ) );
- }
-
- /**
- * @param bool $is_admin
- */
- private function set_is_admin( bool $is_admin ) {
- global $current_screen;
-
- if ( ! $is_admin ) {
- $current_screen = null; // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
- return;
- }
-
- // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
- $current_screen = $this->getMockBuilder( \stdClass::class )
- ->setMethods( [ 'in_admin' ] )
- ->getMock();
-
- $current_screen->method( 'in_admin' )->willReturn( $is_admin );
- }
-}
diff --git a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
index 3fc4a56c8f6..fb95bcf1591 100644
--- a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
+++ b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
@@ -7,7 +7,9 @@
use WCPay\Constants\Country_Code;
use WCPay\Constants\Intent_Status;
+use WCPay\Core\Server\Request\Create_And_Confirm_Intention;
use WCPay\Exceptions\API_Exception;
+use WCPay\Exceptions\API_Merchant_Exception;
use WCPay\Internal\Logger;
use WCPay\Exceptions\Connection_Exception;
use WCPay\Fraud_Prevention\Fraud_Prevention_Service;
@@ -1195,6 +1197,24 @@ public function test_get_tracking_info() {
$this->assertEquals( $expect, $result );
}
+ public function test_throws_api_merchant_exception() {
+ $mock_response = [];
+ $mock_response['error']['code'] = 'card_declined';
+ $mock_response['error']['payment_intent']['charges']['data'][0]['outcome']['seller_message'] = 'Bank declined';
+ $this->set_http_mock_response(
+ 401,
+ $mock_response
+ );
+
+ try {
+ // This is a dummy call to trigger the response so that our test can validate the exception.
+ $this->payments_api_client->create_subscription();
+ } catch ( API_Merchant_Exception $e ) {
+ $this->assertSame( 'card_declined', $e->get_error_code() );
+ $this->assertSame( 'Bank declined', $e->get_merchant_message() );
+ }
+ }
+
/**
* Set up http mock response.
*
diff --git a/woocommerce-payments.php b/woocommerce-payments.php
index 19012d26053..c606a36f8b9 100644
--- a/woocommerce-payments.php
+++ b/woocommerce-payments.php
@@ -11,7 +11,7 @@
* WC tested up to: 9.4.0
* Requires at least: 6.0
* Requires PHP: 7.3
- * Version: 8.6.0
+ * Version: 8.6.1
* Requires Plugins: woocommerce
*
* @package WooCommerce\Payments
@@ -78,6 +78,12 @@ function wcpay_jetpack_init() {
if ( ! wcpay_check_old_jetpack_version() ) {
return;
}
+ $connection_version = Automattic\Jetpack\Connection\Package_Version::PACKAGE_VERSION;
+
+ $custom_content = version_compare( $connection_version, '6.1.0', '>' ) ?
+ 'wcpay_get_jetpack_idc_custom_content' :
+ wcpay_get_jetpack_idc_custom_content();
+
$jetpack_config = new Automattic\Jetpack\Config();
$jetpack_config->ensure(
'connection',
@@ -90,7 +96,7 @@ function wcpay_jetpack_init() {
'identity_crisis',
[
'slug' => 'woocommerce-payments',
- 'customContent' => wcpay_get_jetpack_idc_custom_content(),
+ 'customContent' => $custom_content,
'logo' => plugins_url( 'assets/images/logo.svg', WCPAY_PLUGIN_FILE ),
'admin_page' => '/wp-admin/admin.php?page=wc-admin',
'priority' => 5,