Merge tag '5.13.2'

This commit is contained in:
2025-08-22 15:42:52 -05:00
252 changed files with 4819 additions and 1260 deletions

View File

@@ -1,13 +1,13 @@
#!/bin/bash
# Fetch the versions of WooCommerce from the WordPress API
VERSIONS=$(curl -s https://api.wordpress.org/plugins/info/1.0/woocommerce.json | jq -r '.versions | keys_unsorted | .[]' | grep -v 'trunk')
LATEST_VERSION=""
# Find the latest version
for version in $VERSIONS; do
LATEST_VERSION=$version
done
LATEST_VERSION=$(
curl -s https://api.wordpress.org/plugins/info/1.0/woocommerce.json | \
jq -r '.versions | keys_unsorted | .[]' | \
grep -v 'trunk' | \
sort -V | \
tail -n 1
)
# Check if the latest version is a beta/RC version
if [[ $LATEST_VERSION != *'beta'* && $LATEST_VERSION != *'rc'* ]]; then

View File

@@ -197,10 +197,10 @@ jobs:
- run:
name: Download additional WP Plugins for tests
command: |
./do download:woo-commerce-zip 9.8.5
./do download:woo-commerce-subscriptions-zip 7.5.0
./do download:woo-commerce-zip 10.0.4
./do download:woo-commerce-subscriptions-zip 7.7.0
./do download:woo-commerce-memberships-zip
./do download:automate-woo-zip 6.1.13
./do download:automate-woo-zip 6.1.15
- run:
name: Dump tests ENV variables for acceptance tests
command: |
@@ -468,7 +468,7 @@ jobs:
name: Download WooCommerce Core
command: |
cd ../tests_env/docker
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip ${WOOCOMMERCE_VERSION}" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip ${WOOCOMMERCE_VERSION}" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_WOOCOMMERCE_TOKEN=${WP_GITHUB_WOOCOMMERCE_TOKEN} codeception_acceptance
- when:
condition: << parameters.woo_subscriptions_version >>
steps:
@@ -476,7 +476,7 @@ jobs:
name: Download WooCommerce Subscriptions
command: |
cd ../tests_env/docker
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-subscriptions-zip << parameters.woo_subscriptions_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-subscriptions-zip << parameters.woo_subscriptions_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_WOOCOMMERCE_TOKEN=${WP_GITHUB_WOOCOMMERCE_TOKEN} codeception_acceptance
- when:
condition: << parameters.automate_woo_version >>
steps:
@@ -484,7 +484,7 @@ jobs:
name: Download AutomateWoo
command: |
cd ../tests_env/docker
docker compose run --rm -w /project --entrypoint "./do download:automate-woo-zip << parameters.automate_woo_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
docker compose run --rm -w /project --entrypoint "./do download:automate-woo-zip << parameters.automate_woo_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_WOOCOMMERCE_TOKEN=${WP_GITHUB_WOOCOMMERCE_TOKEN} codeception_acceptance
- run:
name: Group acceptance tests
command: |
@@ -753,7 +753,7 @@ jobs:
name: Download WooCommerce Core
command: |
cd ../tests_env/docker
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip ${WOOCOMMERCE_VERSION}" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip ${WOOCOMMERCE_VERSION}" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_WOOCOMMERCE_TOKEN=${WP_GITHUB_WOOCOMMERCE_TOKEN} codeception_integration
- when:
condition: << parameters.woo_subscriptions_version >>
steps:
@@ -761,7 +761,7 @@ jobs:
name: Download WooCommerce Subscriptions
command: |
cd ../tests_env/docker
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-subscriptions-zip << parameters.woo_subscriptions_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-subscriptions-zip << parameters.woo_subscriptions_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_WOOCOMMERCE_TOKEN=${WP_GITHUB_WOOCOMMERCE_TOKEN} codeception_integration
- when:
condition: << parameters.automate_woo_version >>
steps:
@@ -769,7 +769,7 @@ jobs:
name: Download AutomateWoo
command: |
cd ../tests_env/docker
docker compose run --rm -w /project --entrypoint "./do download:automate-woo-zip << parameters.automate_woo_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
docker compose run --rm -w /project --entrypoint "./do download:automate-woo-zip << parameters.automate_woo_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_WOOCOMMERCE_TOKEN=${WP_GITHUB_WOOCOMMERCE_TOKEN} codeception_integration
- run:
name: 'PHP Integration tests'
command: |
@@ -1182,11 +1182,11 @@ workflows:
- acceptance_tests:
<<: *slack-fail-post-step
name: acceptance_oldest
woo_core_version: 9.7.1
woo_subscriptions_version: 7.4.0
woo_core_version: 9.9.5
woo_subscriptions_version: 7.6.0
automate_woo_version: 6.0.33
mysql_command: --max_allowed_packet=100M
mysql_image: mysql:5.5
mysql_image: mysql:5.6
codeception_image_version: 7.4-cli_20220605.0
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI
wordpress_version: 6.7.2
@@ -1222,14 +1222,14 @@ workflows:
- integration_tests:
<<: *slack-fail-post-step
name: integration_oldest
woo_core_version: 9.7.1
woo_subscriptions_version: 7.4.0
woo_core_version: 9.9.5
woo_subscriptions_version: 7.6.0
automate_woo_version: 6.0.33
codeception_image_version: 7.4-cli_20220605.0
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI # We use image with PHP 7.4 and install required WordPress version via CLI
wordpress_version: 6.7.2
mysql_command: --max_allowed_packet=100M
mysql_image: mysql:5.5
mysql_image: mysql:5.6
requires:
- build
- build_premium:
@@ -1284,8 +1284,8 @@ workflows:
- acceptance_tests:
<<: *slack-fail-post-step
name: acceptance_with_premium_oldest
woo_core_version: 9.7.1
woo_subscriptions_version: 7.4.0
woo_core_version: 9.9.5
woo_subscriptions_version: 7.6.0
automate_woo_version: 6.0.33
codeception_image_version: 7.4-cli_20220605.0
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI
@@ -1295,14 +1295,14 @@ workflows:
- integration_tests:
<<: *slack-fail-post-step
name: integration_with_premium_oldest
woo_core_version: 9.7.1
woo_subscriptions_version: 7.4.0
woo_core_version: 9.9.5
woo_subscriptions_version: 7.6.0
automate_woo_version: 6.0.33
codeception_image_version: 7.4-cli_20220605.0
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI
wordpress_version: 6.7.2
mysql_command: --max_allowed_packet=100M
mysql_image: mysql:5.5
mysql_image: mysql:5.6
requires:
- build_premium

View File

@@ -24,7 +24,7 @@ _N/A_
## Tasks
- [ ] I have added a changelog entry (pcNwfB-4Ov-p2#how-to-write-a-changelog)
- [ ] I have added a changelog entry (`./do changelog:add --type=<type> --description=<description>`)
- [ ] I followed [best practices](https://codex.wordpress.org/I18n_for_WordPress_Developers) for translations
- [ ] I added sufficient test coverage
- [ ] I embraced TypeScript by either creating new files in TypeScript or converting existing JavaScript files when making changes

View File

@@ -118,10 +118,21 @@ function replacePrivatePluginVersion(
string $configParameterName,
string $versionsFilename
): void {
// Read the GitHub token from environment variable
$token = getenv('GH_TOKEN');
// Read the GitHub token from environment variable. Set at https://github.com/mailpoet/mailpoet/settings/secrets/actions.
// If the repository is woocommerce, use the WooCommerce token, otherwise use the general token.
$isWooCommerceRepository = strpos($repository, 'woocommerce/') !== false;
if ($isWooCommerceRepository) {
$token = getenv('WP_GITHUB_WOOCOMMERCE_TOKEN');
} else {
$token = getenv('WP_GITHUB_TOKEN');
}
if (!$token) {
die("GitHub token not found. Make sure it's set in the environment variable 'GH_TOKEN'.");
if ($isWooCommerceRepository) {
die("WooCommerce token not found. For WooCommerce repositories requests, make sure to set the token in the environment variable 'WP_GITHUB_WOOCOMMERCE_TOKEN'.");
} else {
die("GitHub token not found. Make sure it's set in the environment variable 'WP_GITHUB_TOKEN'.");
}
}
$page = 1;

46
dev/php84/Dockerfile Normal file
View File

@@ -0,0 +1,46 @@
FROM wordpress:php8.4-apache
ARG UID=1000
ARG GID=1000
# additinal extensions
RUN apt-get update \
&& apt-get install -y git zlib1g-dev libzip-dev zip wget gnupg msmtp libpng-dev gettext subversion \
&& \
# Install NodeJS, enable Corepack
curl -sL https://deb.nodesource.com/setup_19.x | bash - && \
apt-get install -y nodejs build-essential && \
corepack enable && \
\
# Install WP-CLI
curl -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
chmod +x /usr/local/bin/wp && \
\
# Clean up
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY dev/php.ini /usr/local/etc/php/conf.d/php_user.ini
# msmtp config
RUN printf "account default\nhost smtp\nport 1025" > /etc/msmtprc
# xdebug build an config
ENV XDEBUGINI_PATH=/usr/local/etc/php/conf.d/xdebug.ini
RUN git clone -b "3.4.5" --depth 1 https://github.com/xdebug/xdebug.git /usr/src/php/ext/xdebug \
&& docker-php-ext-configure xdebug --enable-xdebug-dev \
&& docker-php-ext-install xdebug \
&& mkdir /tmp/debug
COPY dev/xdebug.ini /tmp/xdebug.ini
RUN cat /tmp/xdebug.ini >> $XDEBUGINI_PATH
# php extensions
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install mysqli
# allow .htaccess files (between <Directory /var/www/> and </Directory>, which is WordPress installation)
RUN sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
# ensure existing content in /var/www/html respects UID and GID, give Node permissions for Corepack
RUN chown -R ${UID}:${GID} /var/www/html && \
mkdir -p /.node && chown -R ${UID}:${GID} /.node

View File

@@ -34,6 +34,9 @@ WP_CIRCLECI_TOKEN=
WP_GITHUB_USERNAME=
WP_GITHUB_TOKEN=
# GitHub fine-grained token for the WooCommerce organization. Fetch it from the Secret Store by searching for "MailPoet: GitHub MailPoet CI - WooCommerce token".
WP_GITHUB_WOOCOMMERCE_TOKEN=
# k6 performance test suite
# Get following secrets from the Secret Store, look for "MailPoet: plugin .env":
WP_TEST_PERFORMANCE_DATA_URL=

44
mailpoet/CHANGELOG.md Normal file
View File

@@ -0,0 +1,44 @@
# Changelog System
The `changelog/` directory contains individual changelog entries that are compiled into the final changelog during releases.
## How it works
Create individual changelog files in this directory. Each file represents a single changelog entry.
## File naming convention
Files should be named using the following pattern:
- `YYYY-MM-DD-HH-MM-SS-{type}-{description}.md`
Examples:
- `2024-01-15-14-30-00-fix-undefined-array-key.md`
- `2024-01-15-14-35-00-improve-polylang-support.md`
- `2024-01-15-14-40-00-update-woocommerce-segments.md`
## File format
Each changelog file should contain:
```markdown
# Type: {Added|Improved|Fixed|Changed|Updated|Removed}
# Description
Brief description of the change
```
## Types
- `Added`: New features
- `Improved`: Enhancements to existing features
- `Fixed`: Bug fixes
- `Changed`: Changes to existing functionality
- `Updated`: Updates to dependencies, requirements, etc.
- `Removed`: Removed features or functionality
## Compilation
During the release process, these individual files are compiled into the final changelog format used in `readme.txt` and `changelog.txt`.

View File

@@ -9,8 +9,9 @@
4. [Coding and Testing](#coding-and-testing)
1. [DI](#di)
2. [PHP-Scoper](#php-scoper)
3. [i18n](#i18n)
4. [Acceptance testing](#acceptance-testing)
3. [Changelog](#changelog)
4. [i18n](#i18n)
5. [Acceptance testing](#acceptance-testing)
## MailPoet
@@ -121,6 +122,8 @@ $ ./do qa # PHP and JS linters.
$ ./do release:changelog-get [--version-name=...] # Prints out changelog and release notes for given version or for newest version.
$ ./do release:changelog-update [--version-name=...] [--quiet] # Updates changelog in readme.txt for given version or for newest version.
$ ./do changelog:add --type=<type> --description=<description> # Creates a new changelog entry
$ ./do changelog:preview [--version=<version>] # Preview compiled changelog for next version
$ ./do container:dump # Generates DI container cache.
@@ -140,6 +143,16 @@ You can check [the docs](https://symfony.com/doc/3.4/components/dependency_injec
We use PHP-Scoper package to prevent plugin libraries conflicts in PHP. Two plugins may be using different versions of a library. PHP-Scoper prefix dependencies namespaces and they are then moved into `vendor-prefixed` directory.
Dependencies handled by PHP-Scoper are configured in extra configuration files `prefixer/composer.json` and `prefixer/scoper.inc.php`. Installation and processing is triggered in post scripts of the main `composer.json` file.
### Changelog
Create changelog entries using:
```bash
./do changelog:add --type=Fixed --description="Brief description of the change"
```
See [readme](changelog/README.md) for detailed documentation.
### i18n
We use functions `__()`, `_n()`, `_x()`, and `_nx()` with domain `mailpoet` to translate strings. Please follow [best practices](https://codex.wordpress.org/I18n_for_WordPress_Developers).

View File

@@ -523,7 +523,9 @@ class RoboFile extends \Robo\Tasks {
}
public function containerDump() {
define('ABSPATH', getenv('WP_ROOT') . '/');
if (!defined('ABSPATH')) {
define('ABSPATH', getenv('WP_ROOT') . '/');
}
if (!file_exists(ABSPATH . 'wp-config.php')) {
$this->yell('WP_ROOT env variable does not contain valid path to wordpress root.', 40, 'red');
exit(1);
@@ -1209,6 +1211,39 @@ class RoboFile extends \Robo\Tasks {
$this->say("Changelog \n{$changelog}");
}
public function changelogAdd($opts = ['type' => '', 'description' => '']) {
$type = $opts['type'];
$description = $opts['description'];
if (empty($type)) {
$this->say('Please specify a type with --type=<type>');
$this->say('Valid types: Added, Improved, Fixed, Changed, Updated, Removed');
exit(1);
}
if (empty($description)) {
$this->say('Please specify a description with --description=<description>');
exit(1);
}
$changelogger = new \MailPoetTasks\Release\Changelogger();
$filePath = $changelogger->createChangelogEntry($type, $description);
$this->say("Changelog entry created: $filePath");
}
public function changelogPreview($version = null) {
if (!$version) {
$version = $this->releaseVersionGetNext($version);
}
$changelogger = new \MailPoetTasks\Release\Changelogger();
$changelog = $changelogger->compileChangelog($version);
$this->say("Preview changelog for version $version:");
$this->say($changelog);
}
public function releaseVerifyDownloadedZip($version) {
$this->say('Verifying ZIP file');
$zip = new ZipArchive();
@@ -1276,29 +1311,33 @@ class RoboFile extends \Robo\Tasks {
}
public function downloadWooCommerceMembershipsZip(): void {
if (!getenv('WP_GITHUB_USERNAME') && !getenv('WP_GITHUB_TOKEN')) {
$token = getenv('WP_GITHUB_WOOCOMMERCE_TOKEN');
if (!getenv('WP_GITHUB_USERNAME') && !$token) {
$this->yell("Skipping download of WooCommerce Memberships", 40, 'red');
exit(0); // Exit with 0 since it is a valid state for some environments
}
$this->createGithubClient('woocommerce/all-plugins')
$this->createGithubClient('woocommerce/all-plugins', $token)
->downloadRawFile('https://api.github.com/repos/woocommerce/all-plugins/contents/product-packages/woocommerce-memberships/woocommerce-memberships.zip?ref=master', 'woocommerce-memberships.zip', __DIR__ . '/tests/plugins/');
}
public function downloadWooCommerceSubscriptionsZip($tag = null) {
if (!getenv('WP_GITHUB_USERNAME') && !getenv('WP_GITHUB_TOKEN')) {
$token = getenv('WP_GITHUB_WOOCOMMERCE_TOKEN');
if (!getenv('WP_GITHUB_USERNAME') && !$token) {
$this->yell("Skipping download of WooCommerce Subscriptions", 40, 'red');
exit(0); // Exit with 0 since it is a valid state for some environments
}
$this->createGithubClient('woocommerce/woocommerce-subscriptions')
$this->createGithubClient('woocommerce/woocommerce-subscriptions', $token)
->downloadReleaseZip('woocommerce-subscriptions.zip', __DIR__ . '/tests/plugins/', $tag);
}
public function downloadAutomateWooZip($tag = null) {
if (!getenv('WP_GITHUB_USERNAME') && !getenv('WP_GITHUB_TOKEN')) {
$token = getenv('WP_GITHUB_WOOCOMMERCE_TOKEN');
if (!getenv('WP_GITHUB_USERNAME') && !$token) {
$this->yell("Skipping download of Automate Woo", 40, 'red');
exit(0); // Exit with 0 since it is a valid state for some environments
}
$this->createGithubClient('woocommerce/automatewoo')
$this->createGithubClient('woocommerce/automatewoo', $token)
->downloadReleaseZip('automatewoo.zip', __DIR__ . '/tests/plugins/', $tag);
}
@@ -1454,11 +1493,12 @@ class RoboFile extends \Robo\Tasks {
);
}
protected function createGitHubController($project = \MailPoetTasks\Release\GitHubController::PROJECT_MAILPOET) {
protected function createGitHubController($project = \MailPoetTasks\Release\GitHubController::PROJECT_MAILPOET, $token = null) {
$help = "Use your GitHub username and a token from https://github.com/settings/tokens with 'repo' scopes.";
$token = $token ?: $this->getEnv('WP_GITHUB_TOKEN', $help);
return new \MailPoetTasks\Release\GitHubController(
$this->getEnv('WP_GITHUB_USERNAME', $help),
$this->getEnv('WP_GITHUB_TOKEN', $help),
$token,
$project
);
}
@@ -1495,12 +1535,13 @@ class RoboFile extends \Robo\Tasks {
return $exitCode;
}
private function createGithubClient($repositoryName) {
private function createGithubClient($repositoryName, $token = null) {
require_once __DIR__ . '/tasks/GithubClient.php';
$token = $token ?: $this->getEnv('WP_GITHUB_TOKEN');
return new \MailPoetTasks\GithubClient(
$repositoryName,
getenv('WP_GITHUB_USERNAME') ?: null,
getenv('WP_GITHUB_TOKEN') ?: null
$token
);
}
@@ -1510,7 +1551,9 @@ class RoboFile extends \Robo\Tasks {
}
private function createDoctrineEntityManager() {
define('ABSPATH', getenv('WP_ROOT') . '/');
if (!defined('ABSPATH')) {
define('ABSPATH', getenv('WP_ROOT') . '/');
}
if (\MailPoet\Config\Env::$dbPrefix === null) {
/**
* Ensure some prefix is set

View File

@@ -21,11 +21,33 @@
}
}
.mailpoet_social_icons_container {
> input {
vertical-align: top;
&:checked
~ #mailpoet_social_icons_styles
.mailpoet_social_icon_set:not([data-setname^='official']) {
display: block;
}
}
> label {
display: inline-block;
margin: -0.25rem 0 0.5rem;
vertical-align: top;
}
}
.mailpoet_social_icon_set {
border: 1px solid transparent;
margin-bottom: 5px;
padding: 5px;
&:not([data-setname^='official']) {
display: none;
}
&:hover {
border: 1px solid $editor-social-icon-border-color-hover;
}

View File

@@ -243,3 +243,9 @@ progress::-moz-progress-bar {
.mailpoet-form-field-disabled {
cursor: not-allowed;
}
.mailpoet-password-input-toggle {
border-radius: 0 3px 3px 0 !important;
height: 100%;
padding: 0 10px !important;
}

View File

@@ -96,3 +96,12 @@ tr {
font-size: $font-size;
}
}
.mailpoet-notice-container {
.mailpoet_notice {
box-sizing: border-box;
margin: 0 0 1em;
max-width: $grid-column;
padding-right: 38px;
}
}

View File

@@ -62,31 +62,6 @@
background: $color-badge-green;
}
.mailpoet_badge_video {
background: $color-badge-video-guide;
display: inline-block;
line-height: 20px;
padding: 3px 6px;
text-decoration: none;
vertical-align: top;
&:hover,
&:active,
&:focus {
background: $color-badge-green;
color: #fff;
}
.dashicons {
font-size: 14px;
line-height: 20px;
}
}
.mailpoet_badge_video_grey {
background: #c3c3c3;
}
// h1.title needed to override WP styles specificity
h1.title.mailpoet-newsletter-listing-heading {
margin-bottom: $grid-gap;

View File

@@ -165,8 +165,3 @@ ul.sending-method-benefits {
.mailpoet-verify-key-button {
height: 36px;
}
.mailpoet-premium-key-toggle {
height: 34px;
padding: 0 10px !important;
}

View File

@@ -3,14 +3,16 @@
border: 1px solid $color-input-border !important;
border-radius: $form-control-border-radius !important;
box-sizing: border-box;
display: flex;
max-width: 100%;
min-height: $form-control-height;
min-width: 0;
width: $grid-column;
// To align the left padding with the other inputs
input[type='text'].components-form-token-field__input {
line-height: 16px;
margin-left: 0;
min-height: 30px;
min-height: 24px;
padding-left: 8px;
}
// For better fit when the last item is active

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

View File

@@ -1,6 +1,6 @@
import { useContext, useState } from 'react';
import { DropdownMenu } from '@wordpress/components';
import { moreVertical, trash } from '@wordpress/icons';
import { moreVertical, trash, copy } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { Hooks } from 'wp-js-hooks';
import { PremiumModal } from 'common/premium-modal';
@@ -15,10 +15,43 @@ type Props = {
export function StepMoreMenu({ step }: Props): JSX.Element {
const { context } = useContext(AutomationContext);
const [showModal, setShowModal] = useState(false);
const [showDuplicateModal, setShowDuplicateModal] = useState(false);
const canDuplicate = step.key !== 'core:if-else' && step.type !== 'trigger';
const moreControls: StepMoreControlsType = Hooks.applyFilters(
'mailpoet.automation.step.more-controls',
{
...(canDuplicate && {
duplicate: {
key: 'duplicate',
control: {
title: __('Duplicate step', 'mailpoet'),
icon: copy,
onClick: () => setShowDuplicateModal(true),
},
slot: () => {
if (!showDuplicateModal) {
return false;
}
return (
<PremiumModal
onRequestClose={() => {
setShowDuplicateModal(false);
}}
tracking={{
utm_medium: 'upsell_modal',
utm_campaign: 'duplicate_automation_step',
}}
>
{__(
'Duplicating automation steps is available in premium plans. Upgrade to unlock this feature.',
'mailpoet',
)}
</PremiumModal>
);
},
},
}),
delete: {
key: 'delete',
control: {

View File

@@ -2,6 +2,7 @@ import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { plus } from '@wordpress/icons';
import { useCallback, useEffect, useState } from 'react';
import { store as noticesStore } from '@wordpress/notices';
import { Button } from '../../../components/button';
import { storeName } from '../../../../../editor/store';
import { MailPoet } from '../../../../../../mailpoet';
@@ -30,6 +31,8 @@ export function EditNewsletter(): JSX.Element {
const [redirectToTemplateSelection, setRedirectToTemplateSelection] =
useState(false);
const [fetchingPreviewLink, setFetchingPreviewLink] = useState(false);
const [isHandlingDuplicatedStep, setIsHandlingDuplicatedStep] =
useState(false);
const { selectedStep, automationId, savedState, errors } = useSelect(
(select) => ({
@@ -47,6 +50,7 @@ export function EditNewsletter(): JSX.Element {
const automationStepId = selectedStep.id;
const errorFields = errors?.fields ?? {};
const emailIdError = errorFields?.email_id ?? '';
const isDuplicatedStep = selectedStep?.args?.stepDuplicated === true;
const createEmail = useCallback(async () => {
setRedirectToTemplateSelection(true);
@@ -74,6 +78,61 @@ export function EditNewsletter(): JSX.Element {
void dispatch(storeName).save();
}, [automationId, automationStepId]);
const handleDuplicatedStep = useCallback(async (): Promise<number | null> => {
try {
// Save the automation to trigger backend duplication
const savedData = await dispatch(storeName).save();
const newSelectedStep = savedData.automation.steps[automationStepId];
const newEmailId = Number(newSelectedStep?.args?.email_id);
if (newEmailId && !Number.isNaN(newEmailId)) {
return newEmailId;
}
throw new Error('Failed to retrieve new email ID after duplication');
} catch (error) {
void dispatch(noticesStore).createErrorNotice(
__('Email duplication failed. Please try again.', 'mailpoet'),
{ explicitDismiss: true },
);
return null;
}
}, [automationStepId]);
const handleEditContent = useCallback(async () => {
// Ensure we have a valid selected step
if (!selectedStep?.args?.email_id) {
return;
}
// Ensure email ID is a valid number to prevent injection
const currentEmailId = Number(selectedStep.args.email_id);
if (!currentEmailId || Number.isNaN(currentEmailId)) {
return;
}
let newUrl = `?page=mailpoet-newsletter-editor&id=${currentEmailId}&context=automation`;
if (isDuplicatedStep) {
setIsHandlingDuplicatedStep(true);
const newEmailId = await handleDuplicatedStep();
if (newEmailId) {
newUrl = `?page=mailpoet-newsletter-editor&id=${newEmailId}&context=automation`;
} else {
// If duplication failed, don't redirect and let user see the error
setIsHandlingDuplicatedStep(false);
return;
}
setIsHandlingDuplicatedStep(false);
}
window.location.href = newUrl;
}, [isDuplicatedStep, selectedStep?.args?.email_id, handleDuplicatedStep]);
// This component is rendered only when no email ID is set. Once we have the ID
// and the automation is saved, we can safely redirect to the email design flow.
useEffect(() => {
@@ -112,9 +171,9 @@ export function EditNewsletter(): JSX.Element {
<Button
variant="sidebar-primary"
centered
href={`?page=mailpoet-newsletter-editor&id=${
selectedStep.args.email_id as string
}&context=automation`}
onClick={handleEditContent}
isBusy={isHandlingDuplicatedStep}
disabled={isHandlingDuplicatedStep}
>
{__('Edit content', 'mailpoet')}
</Button>

View File

@@ -3,6 +3,7 @@ import { step as OrderStatusChanged } from './steps/order-status-changed';
import { step as OrderCompletedTrigger } from './steps/order-completed';
import { step as OrderCancelledTrigger } from './steps/order-cancelled';
import { step as OrderCreatedTrigger } from './steps/order-created';
import { step as OrderNoteAddedTrigger } from './steps/order-note-added';
import { step as AbandonedCartTrigger } from './steps/abandoned-cart';
import { MailPoet } from '../../../mailpoet';
import { step as BuysAProductTrigger } from './steps/buys-a-product';
@@ -19,6 +20,7 @@ export const initialize = (): void => {
registerStepType(OrderCompletedTrigger);
registerStepType(OrderCancelledTrigger);
registerStepType(OrderCreatedTrigger);
registerStepType(OrderNoteAddedTrigger);
registerStepType(AbandonedCartTrigger);
registerStepType(BuysAProductTrigger);
registerStepType(BuysFromACategory);

View File

@@ -0,0 +1,56 @@
import { __ } from '@wordpress/i18n';
import { dispatch, useSelect } from '@wordpress/data';
import { PanelBody, SelectControl, TextControl } from '@wordpress/components';
import { PlainBodyTitle } from '../../../../../editor/components';
import { storeName } from '../../../../../editor/store';
export function Edit(): JSX.Element {
const { selectedStep } = useSelect(
(select) => ({
selectedStep: select(storeName).getSelectedStep(),
}),
[],
);
const noteType = (selectedStep.args?.note_type as string) ?? 'all';
const noteContains = (selectedStep.args?.note_contains as string) ?? '';
return (
<PanelBody opened>
<PlainBodyTitle title={__('Trigger settings', 'mailpoet')} />
<SelectControl
label={__('Note type', 'mailpoet')}
value={noteType}
options={[
{ label: __('All', 'mailpoet'), value: 'all' },
{ label: __('Note to customer', 'mailpoet'), value: 'customer' },
{ label: __('Private note', 'mailpoet'), value: 'private' },
]}
onChange={(value) => {
void dispatch(storeName).updateStepArgs(
selectedStep.id,
'note_type',
value,
);
}}
/>
<TextControl
label={__('Note contains text', 'mailpoet')}
help={__(
'Only trigger this workflow if the order note contains the certain text. This field is optional.',
'mailpoet',
)}
value={noteContains}
onChange={(value) => {
void dispatch(storeName).updateStepArgs(
selectedStep.id,
'note_contains',
value,
);
}}
/>
</PanelBody>
);
}

View File

@@ -0,0 +1,31 @@
import { __, _x } from '@wordpress/i18n';
import { commentContent } from '@wordpress/icons';
import { StepType } from '../../../../editor/store';
import { Edit } from './edit';
const keywords = [
__('woocommerce', 'mailpoet'),
// translators: noun, used as a search keyword for "Order note added" trigger
__('order', 'mailpoet'),
// translators: noun, used as a search keyword for "Order note added" trigger
__('note', 'mailpoet'),
// translators: noun, used as a search keyword for "Order note added" trigger
__('comment', 'mailpoet'),
];
export const step: StepType = {
key: 'woocommerce:order-note-added',
group: 'triggers',
title: () => __('Order note added', 'mailpoet'),
description: () =>
__(
'Fires when any note is added to an order, can include both private notes and notes to the customer. These notes appear on the right of the order edit screen.',
'mailpoet',
),
subtitle: () => _x('Trigger', 'noun', 'mailpoet'),
keywords,
foreground: '#2271b1',
background: '#f0f6fc',
icon: () => commentContent,
edit: () => <Edit />,
} as const;

View File

@@ -2,3 +2,4 @@ export * from './select/select';
export * from './input/input';
export * from './toggle/toggle';
export * from './checkbox/checkbox';
export * from './input/password-input';

View File

@@ -2,7 +2,7 @@ import { InputHTMLAttributes } from 'react';
import classnames from 'classnames';
import { Tooltip } from 'common/tooltip/tooltip';
type Props = InputHTMLAttributes<HTMLInputElement> & {
export type Props = InputHTMLAttributes<HTMLInputElement> & {
customLabel?: string;
dimension?: 'small';
isFullWidth?: boolean;

View File

@@ -0,0 +1,35 @@
import { _x, __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { Button } from 'common/button/button';
import { Props as InputProps, Input } from './input';
type Props = InputProps & {
forceRevealed?: boolean;
};
export function PasswordInput({ forceRevealed = false, ...attributes }: Props) {
const [isRevealed, setIsRevealed] = useState(false);
const inputType = forceRevealed || isRevealed ? 'text' : 'password';
const toggleButton = !forceRevealed && (
<Button
className="mailpoet-password-input-toggle"
variant="tertiary"
aria-label={
isRevealed
? __('Hide input value', 'mailpoet')
: __('Show input value', 'mailpoet')
}
onClick={() => setIsRevealed(!isRevealed)}
>
{isRevealed
? // translators: Used as a button to show or hide the password
_x('Hide', 'verb', 'mailpoet')
: // translators: Used as a button to show or hide the password
_x('Show', 'verb', 'mailpoet')}
</Button>
);
return <Input type={inputType} iconEnd={toggleButton} {...attributes} />;
}

View File

@@ -1,7 +1,5 @@
import { _x } from '@wordpress/i18n';
import { Button, Input } from 'common/index';
import { PasswordInput } from 'common/index';
import { useAction, useSelector } from 'settings/store/hooks';
import { useState } from 'react';
type KeyInputPropType = {
placeholder?: string;
@@ -16,25 +14,9 @@ export function KeyInput({
}: KeyInputPropType) {
const state = useSelector('getKeyActivationState')();
const setState = useAction('updateKeyActivationState');
const [isRevealed, setIsRevealed] = useState(false);
const inputType = forceRevealed || isRevealed ? 'text' : 'password';
const toggleButton = !forceRevealed && (
<Button
className="mailpoet-premium-key-toggle"
variant="tertiary"
onClick={() => setIsRevealed(!isRevealed)}
>
{isRevealed
? // translators: Used as a button to show or hide the premium key
_x('Hide', 'verb', 'mailpoet')
: // translators: Used as a button to show or hide the premium key
_x('Show', 'verb', 'mailpoet')}
</Button>
);
return (
<Input
type={inputType}
<PasswordInput
forceRevealed={forceRevealed}
id="mailpoet_premium_key"
name="premium[premium_key]"
placeholder={placeholder}
@@ -48,7 +30,6 @@ export function KeyInput({
key: event.target.value.trim() || null,
})
}
iconEnd={toggleButton}
/>
);
}

View File

@@ -1,6 +1,5 @@
export const FeaturesController = (config) => ({
FEATURE_BRAND_TEMPLATES: 'brand_templates',
FEATURE_ODIE_CHATBOT: 'odie_chatbot',
isSupported: (feature) => {
return config[feature] || false;

View File

@@ -233,7 +233,9 @@ export function* showPreview() {
yield changeActiveSidebar('default');
const customFields = select(storeName).getAllAvailableCustomFields();
const formData = select(storeName).getFormData();
const formBlocks = select(storeName).getFormBlocks();
// Get blocks directly from block editor store to ensure we have the latest state
const formBlocks = select(blockEditorStore).getBlocks();
const blocksToFormBody = blocksToFormBodyFactory(
FONT_SIZES,
SETTINGS_DEFAULTS.colors,

View File

@@ -33,7 +33,7 @@ export type FormSettingsType = {
fixedBarStyles: PlacementStyles;
fontColor?: string;
fontFamily?: string;
fontSize?: number;
fontSize?: string | number;
formPadding: number;
formPlacement: {
popup: FormPlacementBase & {
@@ -105,7 +105,7 @@ export type InputBlockStyles = {
backgroundColor?: string;
gradient?: string;
borderSize?: number;
fontSize?: number;
fontSize?: string | number;
fontColor?: string;
borderRadius?: number;
borderColor?: string;

View File

@@ -39,7 +39,9 @@ export const mapInputBlockStyles = (styles: InputBlockStylesServerData) => {
mappedStyles.borderSize = Number(styles.border_size);
}
if (has(styles, 'font_size') && styles.font_size !== undefined) {
mappedStyles.fontSize = Number(styles.font_size);
mappedStyles.fontSize = !Number.isNaN(Number(styles.font_size))
? Number(styles.font_size)
: styles.font_size;
}
if (has(styles, 'font_color') && styles.font_color) {
mappedStyles.fontColor = styles.font_color;

View File

@@ -95,7 +95,7 @@ Module.DynamicProductsBlockModel = base.BlockModel.extend({
base.BlockView.prototype.initialize.apply(this, args);
this.on(
'change:amount change:contentType change:terms change:inclusionType change:displayType change:titleFormat change:featuredImagePosition change:titleAlignment change:titleIsLink change:imageFullWidth change:pricePosition change:readMoreType change:readMoreText change:sortBy change:showDivider change:dynamicProductsType change:titlePosition',
'change:amount change:contentType change:terms change:inclusionType change:displayType change:titleFormat change:featuredImagePosition change:titleAlignment change:titleIsLink change:imageFullWidth change:pricePosition change:readMoreType change:readMoreText change:sortBy change:showDivider change:dynamicProductsType change:titlePosition change:excludeOutOfStock',
this._handleChanges,
this,
);
@@ -220,6 +220,10 @@ Module.DynamicProductsBlockSettingsView = base.BlockSettingsView.extend({
this.changeField,
'inclusionType',
),
'change .mailpoet_dynamic_products_exclude_out_of_stock': _.partial(
this.changeBoolCheckboxField,
'excludeOutOfStock',
),
'change .mailpoet_dynamic_products_title_alignment': _.partial(
this.changeField,
'titleAlignment',

View File

@@ -315,6 +315,7 @@ SocialBlockSettingsStylesView = Marionette.View.extend({
socialIconSets: allIconSets.toJSON(),
availableSets: _.keys(allIconSets.toJSON()),
availableSocialIcons: this.model.get('icons').pluck('iconType'),
imageMissingSrc: App.getConfig().get('urls.imageMissing'),
};
},
changeSocialIconSet: function (event) {

View File

@@ -33,7 +33,7 @@ export function NewsletterTypes({
}: Props): JSX.Element {
const navigate = useNavigate();
const [isCreating, setIsCreating] = useState(false);
const [isCreating, setIsCreating] = useState(null);
const [isSelectEditorModalOpen, setIsSelectEditorModalOpen] = useState(false);
const isNewEmailEditorEnabled = window.mailpoet_block_email_editor_enabled;
@@ -47,49 +47,26 @@ export function NewsletterTypes({
}
};
const renderType = (type): JSX.Element => {
const badgeClassName =
window.mailpoet_is_new_user === true
? 'mailpoet_badge mailpoet_badge_video'
: 'mailpoet_badge mailpoet_badge_video mailpoet_badge_video_grey';
return (
<div
key={type.slug}
data-type={type.slug}
className="mailpoet-newsletter-type"
>
<div className="mailpoet-newsletter-type-image" />
<div className="mailpoet-newsletter-type-content">
<Heading level={4}>
{type.title} {type.beta ? `(${__('Beta', 'mailpoet')})` : ''}
</Heading>
<p>{type.description}</p>
{type.videoGuide && (
<a
className={badgeClassName}
href={type.videoGuide}
target="_blank"
rel="noopener noreferrer"
>
<span className="dashicons dashicons-format-video" />
{__('See video guide', 'mailpoet')}
</a>
)}
{type.kbLink && (
<a href={type.kbLink} target="_blank" rel="noopener noreferrer">
{__('Read more.', 'mailpoet')}
</a>
)}
<div className="mailpoet-flex-grow" />
<div className="mailpoet-newsletter-type-action">{type.action}</div>
</div>
const renderType = (type): JSX.Element => (
<div
key={type.slug}
data-type={type.slug}
className="mailpoet-newsletter-type"
>
<div className="mailpoet-newsletter-type-image" />
<div className="mailpoet-newsletter-type-content">
<Heading level={4}>
{type.title} {type.beta ? `(${__('Beta', 'mailpoet')})` : ''}
</Heading>
<p>{type.description}</p>
<div className="mailpoet-flex-grow" />
<div className="mailpoet-newsletter-type-action">{type.action}</div>
</div>
);
};
</div>
);
const createNewsletter = (type): void => {
setIsCreating(true);
setIsCreating(type);
MailPoet.trackEvent('Emails > Type selected', {
'Email type': type,
});
@@ -106,7 +83,7 @@ export function NewsletterTypes({
navigate(`/template/${response.data.id}`);
})
.fail((response) => {
setIsCreating(false);
setIsCreating(null);
if (response.errors.length > 0) {
return <APIErrorsNotice errors={response.errors} />;
}
@@ -124,7 +101,7 @@ export function NewsletterTypes({
're-engagement',
);
const createAutomation = () => {
setIsCreating(true);
setIsCreating('automation');
window.location.href = 'admin.php?page=mailpoet-automation-templates';
};
@@ -133,7 +110,8 @@ export function NewsletterTypes({
<Button
variant="secondary"
onClick={createStandardNewsletter}
isBusy={isCreating}
isBusy={isCreating === 'standard'}
disabled={isCreating !== null}
data-automation-id="create_standard"
>
{__('Create', 'mailpoet')}
@@ -148,6 +126,8 @@ export function NewsletterTypes({
variant="secondary"
className="mailpoet-button-with-wordpress-icon"
onClick={onToggle}
isBusy={isCreating === 'standard'}
disabled={isCreating !== null}
aria-expanded={isOpen}
data-automation-id="create_standard_email_dropdown"
>
@@ -214,7 +194,8 @@ export function NewsletterTypes({
<Button
variant="secondary"
onClick={createAutomation}
isBusy={isCreating}
isBusy={isCreating === 'automation'}
disabled={isCreating !== null}
data-automation-id="create_automation"
>
{__('Create', 'mailpoet')}
@@ -234,7 +215,8 @@ export function NewsletterTypes({
<Button
variant="secondary"
onClick={createNotificationNewsletter}
isBusy={isCreating}
isBusy={isCreating === 'notification'}
disabled={isCreating !== null}
data-automation-id="create_notification"
>
{__('Create', 'mailpoet')}
@@ -252,7 +234,8 @@ export function NewsletterTypes({
<Button
variant="secondary"
onClick={createReEngagementNewsletter}
isBusy={isCreating}
isBusy={isCreating === 're_engagement'}
disabled={isCreating !== null}
data-automation-id="create_notification"
>
{__('Create', 'mailpoet')}

View File

@@ -12,7 +12,7 @@ import { Scheduling } from './scheduling';
import { ListingHeadingStepsRoute } from '../../listings/heading-steps-route';
export function NewsletterTypeReEngagement(): JSX.Element {
let defaultAfterTime = '';
let defaultAfterTime = '11';
if (MailPoet.deactivateSubscriberAfterInactiveDays) {
defaultAfterTime = (
Math.floor(Number(MailPoet.deactivateSubscriberAfterInactiveDays) / 30) -

View File

@@ -1,6 +1,6 @@
import { Label, Inputs } from 'settings/components';
import { t, onChange } from 'common/functions';
import { Input } from 'common/form/input/input';
import { PasswordInput } from 'common/form';
import { Select } from 'common/form/select/select';
import { useSetting, useSelector } from 'settings/store/hooks';
import { SendingFrequency } from './sending-frequency';
@@ -35,9 +35,8 @@ export function AmazonSesFields() {
</Inputs>
<Label title={t('accessKey')} htmlFor="mailpoet_amazon_ses_access_key" />
<Inputs>
<Input
<PasswordInput
dimension="small"
type="text"
value={accessKey}
className="regular-text"
onChange={onChange(setAccessKey)}
@@ -46,9 +45,8 @@ export function AmazonSesFields() {
</Inputs>
<Label title={t('secretKey')} htmlFor="mailpoet_amazon_ses_secret_key" />
<Inputs>
<Input
<PasswordInput
dimension="small"
type="text"
value={secretKey}
className="regular-text"
onChange={onChange(setSecretKey)}

View File

@@ -1,6 +1,6 @@
import { Label, Inputs } from 'settings/components';
import { t, onChange } from 'common/functions';
import { Input } from 'common/form/input/input';
import { PasswordInput } from 'common/form';
import { useSetting, useSelector } from 'settings/store/hooks';
import { SendingFrequency } from './sending-frequency';
@@ -15,9 +15,8 @@ export function SendGridFields() {
/>
<Label title={t('apiKey')} htmlFor="mailpoet_sendgrid_api_key" />
<Inputs>
<Input
<PasswordInput
dimension="small"
type="text"
value={apiKey}
onChange={onChange(setApiKey)}
id="mailpoet_sendgrid_api_key"

View File

@@ -1,19 +1,47 @@
import jQuery from 'jquery';
import { MailPoet } from 'mailpoet';
export const createNewSegment = (onCreateSegment) => {
interface Segment {
id: string;
name: string;
text: string;
subscriberCount: number;
}
interface CreateSegmentResponse {
data: {
id: string;
name: string;
description: string;
};
}
interface ApiErrorResponse {
errors: Array<{
error: string;
message: string;
}>;
}
export const createNewSegment = (
onCreateSegment: (segment: Segment) => void,
): void => {
MailPoet.Modal.popup({
title: MailPoet.I18n.t('addNewList'),
template: jQuery('#new_segment_template').html(),
});
jQuery('#new_segment_name').on('keypress', (e) => {
jQuery('#new_segment_name').on('keypress', (e: JQuery.KeyPressEvent) => {
if (e.which === 13) {
jQuery('#new_segment_process').trigger('click');
}
});
jQuery('#new_segment_process').on('click', () => {
const segmentName = jQuery('#new_segment_name').val().trim();
const segmentDescription = jQuery('#new_segment_description').val().trim();
const segmentName: string =
jQuery('#new_segment_name').val()?.toString().trim() || '';
const segmentDescription: string =
jQuery('#new_segment_description').val()?.toString().trim() || '';
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
@@ -24,7 +52,7 @@ export const createNewSegment = (onCreateSegment) => {
description: segmentDescription,
},
})
.done((response) => {
.done((response: CreateSegmentResponse) => {
onCreateSegment({
id: response.data.id,
name: response.data.name,
@@ -34,15 +62,17 @@ export const createNewSegment = (onCreateSegment) => {
MailPoet.Modal.close();
})
.fail((response) => {
.fail((response: ApiErrorResponse) => {
if (response.errors.length > 0) {
MailPoet.Notice.hide();
MailPoet.Notice.showApiErrorNotice(response, {
positionAfter: '#new_segment_name',
positionAfter: '#new_segment_error_message',
});
}
});
})
.catch(() => {});
});
jQuery('#new_segment_cancel').on('click', () => {
MailPoet.Modal.close();
});

View File

@@ -9,7 +9,7 @@ import {
createSelection,
destroySelection,
} from './generate-segment-selection.jsx';
import { createNewSegment } from './create-new-segment.jsx';
import { createNewSegment } from './create-new-segment';
function SelectSegment({ setSelectedSegments }) {
const { segments: segmentsContext } = useContext(GlobalContext);

View File

@@ -176,232 +176,245 @@ const createModal = (submitModal, closeModal, field, title) => (
</Modal>
);
const getBulkActions = () => [
{
name: 'moveToList',
label: __('Move to list...', 'mailpoet'),
onSelect: function onSelect(submitModal, closeModal) {
const field = {
id: 'move_to_segment',
name: 'move_to_segment',
endpoint: 'segments',
filter: function filter(segment) {
return !!(!segment.deleted_at && segment.type === 'default');
},
};
const getBulkActions = () => {
const bulkActions = [
{
name: 'moveToList',
label: __('Move to list...', 'mailpoet'),
onSelect: function onSelect(submitModal, closeModal) {
const field = {
id: 'move_to_segment',
name: 'move_to_segment',
endpoint: 'segments',
filter: function filter(segment) {
return !!(!segment.deleted_at && segment.type === 'default');
},
};
return createModal(
submitModal,
closeModal,
field,
__('Move to list...', 'mailpoet'),
);
return createModal(
submitModal,
closeModal,
field,
__('Move to list...', 'mailpoet'),
);
},
getData: function getData() {
return {
segment_id: Number(jQuery('#move_to_segment').val()),
};
},
onSuccess: function onSuccess(response: Response) {
MailPoet.Notice.success(
__(
'%1$d subscribers were moved to list <strong>%2$s</strong>.',
'mailpoet',
)
.replace('%1$d', Number(response.meta.count).toLocaleString())
.replace('%2$s', response.meta.segment),
);
},
},
getData: function getData() {
return {
segment_id: Number(jQuery('#move_to_segment').val()),
};
},
onSuccess: function onSuccess(response: Response) {
MailPoet.Notice.success(
__(
'%1$d subscribers were moved to list <strong>%2$s</strong>.',
'mailpoet',
)
.replace('%1$d', Number(response.meta.count).toLocaleString())
.replace('%2$s', response.meta.segment),
);
},
},
{
name: 'addToList',
label: __('Add to list...', 'mailpoet'),
onSelect: function onSelect(submitModal, closeModal) {
const field = {
id: 'add_to_segment',
name: 'add_to_segment',
endpoint: 'segments',
filter: function filter(segment) {
return !!(!segment.deleted_at && segment.type === 'default');
},
};
{
name: 'addToList',
label: __('Add to list...', 'mailpoet'),
onSelect: function onSelect(submitModal, closeModal) {
const field = {
id: 'add_to_segment',
name: 'add_to_segment',
endpoint: 'segments',
filter: function filter(segment) {
return !!(!segment.deleted_at && segment.type === 'default');
},
};
return createModal(
submitModal,
closeModal,
field,
__('Add to list...', 'mailpoet'),
);
return createModal(
submitModal,
closeModal,
field,
__('Add to list...', 'mailpoet'),
);
},
getData: function getData() {
return {
segment_id: Number(jQuery('#add_to_segment').val()),
};
},
onSuccess: function onSuccess(response: Response) {
MailPoet.Notice.success(
__(
'%1$d subscribers were added to list <strong>%2$s</strong>.',
'mailpoet',
)
.replace('%1$d', Number(response.meta.count).toLocaleString())
.replace('%2$s', response.meta.segment),
);
},
},
getData: function getData() {
return {
segment_id: Number(jQuery('#add_to_segment').val()),
};
},
onSuccess: function onSuccess(response: Response) {
MailPoet.Notice.success(
__(
'%1$d subscribers were added to list <strong>%2$s</strong>.',
'mailpoet',
)
.replace('%1$d', Number(response.meta.count).toLocaleString())
.replace('%2$s', response.meta.segment),
);
},
},
{
name: 'removeFromList',
label: __('Remove from list...', 'mailpoet'),
onSelect: function onSelect(submitModal, closeModal) {
const field = {
id: 'remove_from_segment',
name: 'remove_from_segment',
endpoint: 'segments',
filter: function filter(segment) {
return segment.type === 'default';
},
};
{
name: 'removeFromList',
label: __('Remove from list...', 'mailpoet'),
onSelect: function onSelect(submitModal, closeModal) {
const field = {
id: 'remove_from_segment',
name: 'remove_from_segment',
endpoint: 'segments',
filter: function filter(segment) {
return segment.type === 'default';
},
};
return createModal(
submitModal,
closeModal,
field,
__('Remove from list...', 'mailpoet'),
);
return createModal(
submitModal,
closeModal,
field,
__('Remove from list...', 'mailpoet'),
);
},
getData: function getData() {
return {
segment_id: Number(jQuery('#remove_from_segment').val()),
};
},
onSuccess: function onSuccess(response: Response) {
MailPoet.Notice.success(
__(
'%1$d subscribers were removed from list <strong>%2$s</strong>.',
'mailpoet',
)
.replace('%1$d', Number(response.meta.count).toLocaleString())
.replace('%2$s', response.meta.segment),
);
},
},
getData: function getData() {
return {
segment_id: Number(jQuery('#remove_from_segment').val()),
};
{
name: 'removeFromAllLists',
label: __('Remove from all lists', 'mailpoet'),
onSuccess: function onSuccess(response: Response) {
MailPoet.Notice.success(
__(
'%1$d subscribers were removed from all lists.',
'mailpoet',
).replace('%1$d', Number(response.meta.count).toLocaleString()),
);
},
},
onSuccess: function onSuccess(response: Response) {
MailPoet.Notice.success(
__(
'%1$d subscribers were removed from list <strong>%2$s</strong>.',
'mailpoet',
)
.replace('%1$d', Number(response.meta.count).toLocaleString())
.replace('%2$s', response.meta.segment),
);
{
name: 'trash',
label: __('Move to trash', 'mailpoet'),
onSuccess: getMessages().onTrash,
},
},
{
name: 'removeFromAllLists',
label: __('Remove from all lists', 'mailpoet'),
onSuccess: function onSuccess(response: Response) {
MailPoet.Notice.success(
__('%1$d subscribers were removed from all lists.', 'mailpoet').replace(
'%1$d',
Number(response.meta.count).toLocaleString(),
),
);
},
},
{
name: 'trash',
label: __('Move to trash', 'mailpoet'),
onSuccess: getMessages().onTrash,
},
{
name: 'unsubscribe',
label: __('Unsubscribe', 'mailpoet'),
onSelect: (submitModal, closeModal, bulkActionProps) => {
const count =
bulkActionProps.selection !== 'all'
? bulkActionProps.selected_ids.length
: bulkActionProps.count;
return (
<Modal
title={__('Unsubscribe', 'mailpoet')}
onRequestClose={closeModal}
isDismissible
>
<p>
{__(
'This action will unsubscribe %s subscribers from all lists. This action cannot be undone. Are you sure, you want to continue?',
'mailpoet',
).replace('%s', Number(count).toLocaleString())}
</p>
<span className="mailpoet-gap-half" />
<Button
onClick={submitModal}
dimension="small"
variant="secondary"
automationId="bulk-unsubscribe-confirm"
{
name: 'unsubscribe',
label: __('Unsubscribe', 'mailpoet'),
onSelect: (submitModal, closeModal, bulkActionProps) => {
const count =
bulkActionProps.selection !== 'all'
? bulkActionProps.selected_ids.length
: bulkActionProps.count;
return (
<Modal
title={__('Unsubscribe', 'mailpoet')}
onRequestClose={closeModal}
isDismissible
>
{__('Apply', 'mailpoet')}
</Button>
</Modal>
);
<p>
{__(
'This action will unsubscribe %s subscribers from all lists. This action cannot be undone. Are you sure, you want to continue?',
'mailpoet',
).replace('%s', Number(count).toLocaleString())}
</p>
<span className="mailpoet-gap-half" />
<Button
onClick={submitModal}
dimension="small"
variant="secondary"
automationId="bulk-unsubscribe-confirm"
>
{__('Apply', 'mailpoet')}
</Button>
</Modal>
);
},
},
},
{
name: 'addTag',
label: __('Add tag...', 'mailpoet'),
onSelect: function onSelect(submitModal, closeModal) {
const field = {
id: 'add_tag',
name: 'add_tag',
endpoint: 'tags',
};
{
name: 'addTag',
label: __('Add tag...', 'mailpoet'),
onSelect: function onSelect(submitModal, closeModal) {
const field = {
id: 'add_tag',
name: 'add_tag',
endpoint: 'tags',
};
return createModal(
submitModal,
closeModal,
field,
__('Add tag...', 'mailpoet'),
);
return createModal(
submitModal,
closeModal,
field,
__('Add tag...', 'mailpoet'),
);
},
getData: function getData() {
return {
tag_id: Number(jQuery('#add_tag').val()),
};
},
onSuccess: function onSuccess(response: Response) {
MailPoet.Notice.success(
__(
'Tag <strong>%1$s</strong> was added to %2$d subscribers.',
'mailpoet',
)
.replace('%1$s', response.meta.tag)
.replace('%2$d', Number(response.meta.count).toLocaleString()),
);
},
},
getData: function getData() {
return {
tag_id: Number(jQuery('#add_tag').val()),
};
},
onSuccess: function onSuccess(response: Response) {
MailPoet.Notice.success(
__(
'Tag <strong>%1$s</strong> was added to %2$d subscribers.',
'mailpoet',
)
.replace('%1$s', response.meta.tag)
.replace('%2$d', Number(response.meta.count).toLocaleString()),
);
},
},
{
name: 'removeTag',
label: __('Remove tag...', 'mailpoet'),
onSelect: function onSelect(submitModal, closeModal) {
const field = {
id: 'remove_tag',
name: 'remove_tag',
endpoint: 'tags',
};
{
name: 'removeTag',
label: __('Remove tag...', 'mailpoet'),
onSelect: function onSelect(submitModal, closeModal) {
const field = {
id: 'remove_tag',
name: 'remove_tag',
endpoint: 'tags',
};
return createModal(
submitModal,
closeModal,
field,
__('Remove tag...', 'mailpoet'),
);
return createModal(
submitModal,
closeModal,
field,
__('Remove tag...', 'mailpoet'),
);
},
getData: function getData() {
return {
tag_id: Number(jQuery('#remove_tag').val()),
};
},
onSuccess: function onSuccess(response: Response) {
MailPoet.Notice.success(
__(
'Tag <strong>%1$s</strong> was removed from %2$d subscribers.',
'mailpoet',
)
.replace('%1$s', response.meta.tag)
.replace('%2$d', Number(response.meta.count).toLocaleString()),
);
},
},
getData: function getData() {
return {
tag_id: Number(jQuery('#remove_tag').val()),
};
},
onSuccess: function onSuccess(response: Response) {
MailPoet.Notice.success(
__(
'Tag <strong>%1$s</strong> was removed from %2$d subscribers.',
'mailpoet',
)
.replace('%1$s', response.meta.tag)
.replace('%2$d', Number(response.meta.count).toLocaleString()),
);
},
},
];
];
// Filter out 'unsubscribe' action if we're in the unsubscribed group
const url = window.location.href;
const match = url.match(/group\[(.*?)\]/);
const group = match ? match[1] : null;
if (group === 'unsubscribed') {
return bulkActions.filter((action) => action.name !== 'unsubscribe');
}
return bulkActions;
};
const getItemActions = () => [
{

View File

@@ -66,7 +66,7 @@ function WelcomeWizardUsageTrackingStep({ loading, submitForm }) {
<div className="mailpoet-wizard-note">
{ReactStringReplace(
__(
'MailPoet may load Google Fonts, DocsBot and other [link]3rd party libraries[/link].',
'MailPoet may load Google Fonts, WordPress.com and other [link]3rd party libraries[/link].',
'mailpoet',
),
/\[link\](.*?)\[\/link\]/g,

View File

@@ -1,5 +1,55 @@
== Changelog ==
= 5.13.2 - 2025-08-20 =
* Fixed: revert 5.13.1 release.
= 5.13.0 - 2025-08-12 =
* Added: Add duplication of an automation step;
* Added: "Active WooCommerce subscriptions count" field in automation filters;
* Updated: Bump the minimum required WooCommerce version to 10.0 and tested up to version to 10.1;
* Improved: Ensure that logging of WooCommerce First Purchase is done only when necessary.
= 5.12.13 - 2025-08-04 =
* Added: The dynamic products block can filter out out-of-stock products;
* Fixed: Prevent automatic "Subscribed" status for guest order subscribers when Sign-up Confirmation is disabled.
= 5.12.12 - 2025-07-22 =
* Added: A new Automation Trigger when a new WooCommerce order note is added;
* Updated: minimum required WooCommerce version to 9.9 and tested up to version to 10.0.
= 5.12.11 - 2025-07-14 =
* Improved: JavaScript and CSS compatibility with other plugins and themes;
* Fixed: inconsistent spacing when adding tags to subscribers;
* Fixed: error message position when creating a list during import;
* Fixed: page title replacement broken on non-English sites.
= 5.12.10 - 2025-07-07 =
* Improved: SendGrid API key field and Amazon SES access key and secret key fields now use masked input fields to prevent accidental credential exposure;
* Fixed: PHP Warning: Undefined array key “blocks”;
* Fixed: filter `mailpoet_unsubscribe_confirmation_page` not redirecting to the success page.
= 5.12.9 - 2025-06-30 =
* Improved: handling of MailPoet Page title when switching website languages;
* Improved: add default re-engagement emails trigger value when "Inactive subscribers" feature is turned off;
* Improved: when choosing an email type, show loading animation only on the selected one;
* Changed: replaced DocsBot with WordPress.com chatbot when searching MailPoet Knowledge Base;
* Fixed: rendering submit button when font size is changed;
* Fixed: fatal error in ACF and SCF when working with post templates in location rules;
* Fixed: apply the latest changes in the form editor in the preview.
= 5.12.8 - 2025-06-24 =
* Updated: support custom order statuses in WooCommerce segments;
* Improved: adds `polylang` to the list of permitted scripts;
* Fixed: warning `Undefined array key "blocks"`.
= 5.12.7 - 2025-06-16 =
* Updated: minimum required WooCommerce version to 9.8 and tested up to version to 9.9.
= 5.12.6 - 2025-06-09 =
* Added: new networks in social icons block (Behance, Bluesky, Discord, GitHub, Gravatar, Mastodon, Medium, Patreon, Reddit, RSS, Spotify, Telegram, Threads, TikTok, Tumblr, Twitch, Viemo, WhatsApp, WordPress);
* Improved: updated existing social networks with the official icons;
* Improved: hide 'unsubscribe' bulk action when segmenting unsubscribed subscribers.
= 5.12.5 - 2025-06-02 =
* Improved: Add "4th" day of week as a monthly frequency option;
* Fixed: issue where duplicated newsletters could be incorrectly linked to unrelated posts under certain conditions.

View File

@@ -0,0 +1 @@

View File

@@ -78,14 +78,16 @@
"./tools/vendor/composer.phar --working-dir=tasks/phpstan install",
"./tools/vendor/composer.phar --working-dir=../tests_env install",
"php ./tasks/fix-guzzle.php",
"php ./tasks/fix-php82-deprecations.php"
"php ./tasks/fix-php82-deprecations.php",
"php ./tasks/FixPhp84Deprecations.php"
],
"post-install-cmd": [
"./tools/vendor/composer.phar --working-dir=tasks/code_sniffer install",
"./tools/vendor/composer.phar --working-dir=tasks/phpstan install",
"./tools/vendor/composer.phar --working-dir=../tests_env install",
"php ./tasks/fix-guzzle.php",
"php ./tasks/fix-php82-deprecations.php"
"php ./tasks/fix-php82-deprecations.php",
"php ./tasks/FixPhp84Deprecations.php"
],
"pre-autoload-dump": [
"php ./tasks/fix-codeception-stub.php",

View File

@@ -204,7 +204,7 @@ class PageRenderer {
'name' => $tag->getName(),
];
}, $this->tagRepository->findAll()),
'display_docsbot_widget' => $this->displayDocsBotWidget(),
'display_chatbot_widget' => $this->displayChatBotWidget(),
'is_woocommerce_subscriptions_active' => $this->wooCommerceSubscriptionsHelper->isWooCommerceSubscriptionsActive(),
'cron_trigger_method' => $this->settings->get('cron_trigger.method'),
];
@@ -252,7 +252,7 @@ class PageRenderer {
];
}
public function displayDocsBotWidget(): bool {
public function displayChatBotWidget(): bool {
$display = $this->wp->applyFilters('mailpoet_display_docsbot_widget', $this->settings->get('3rd_party_libs.enabled') === '1');
return (bool)$display;
}

View File

@@ -9,6 +9,8 @@ use MailPoet\Captcha\CaptchaConstants;
use MailPoet\Config\ServicesChecker;
use MailPoet\Cron\CronTrigger;
use MailPoet\Entities\DynamicSegmentFilterData;
use MailPoet\Entities\FormEntity;
use MailPoet\Form\FormsRepository;
use MailPoet\Listing\ListingDefinition;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Segments\DynamicSegments\DynamicSegmentFilterRepository;
@@ -106,6 +108,9 @@ class Reporter {
/*** @var ReporterCampaignData */
private $reporterCampaignData;
/** @var FormsRepository */
private $formsRepository;
public function __construct(
NewslettersRepository $newslettersRepository,
SegmentsRepository $segmentsRepository,
@@ -121,7 +126,8 @@ class Reporter {
AutomationStorage $automationStorage,
UnsubscribeReporter $unsubscribeReporter,
DotcomHelperFunctions $dotcomHelperFunctions,
ReporterCampaignData $reporterCampaignData
ReporterCampaignData $reporterCampaignData,
FormsRepository $formsRepository
) {
$this->newslettersRepository = $newslettersRepository;
$this->segmentsRepository = $segmentsRepository;
@@ -138,6 +144,7 @@ class Reporter {
$this->unsubscribeReporter = $unsubscribeReporter;
$this->dotcomHelperFunctions = $dotcomHelperFunctions;
$this->reporterCampaignData = $reporterCampaignData;
$this->formsRepository = $formsRepository;
}
public function getData() {
@@ -150,6 +157,7 @@ class Reporter {
$hasWc = $this->woocommerceHelper->isWooCommerceActive();
$inactiveSubscribersMonths = (int)round((int)$this->settings->get('deactivate_subscriber_after_inactive_days') / 30);
$inactiveSubscribersStatus = $inactiveSubscribersMonths === 0 ? 'never' : "$inactiveSubscribersMonths months";
$activeFormCounts = $this->formsRepository->getActiveFormsCountByType();
$result = [
'PHP version' => PHP_VERSION,
@@ -269,6 +277,18 @@ class Reporter {
'Sign-up confirmation: Confirmation Template > using html email editor template' => (boolean)$this->settings->get(ConfirmationEmailCustomizer::SETTING_ENABLE_EMAIL_CUSTOMIZER, false),
'Is WordPress.com' => $this->dotcomHelperFunctions->isDotcom() ? 'yes' : 'no',
'WordPress.com plan' => $this->dotcomHelperFunctions->getDotcomPlan(),
'Forms > Number of active forms' => $activeFormCounts['all'],
'Forms > Number of active Below pages forms' => $activeFormCounts[FormEntity::DISPLAY_TYPE_BELOW_POST],
'Forms > Number of active Fixed bar forms' => $activeFormCounts[FormEntity::DISPLAY_TYPE_FIXED_BAR],
'Forms > Number of active Pop-up forms' => $activeFormCounts[FormEntity::DISPLAY_TYPE_POPUP],
'Forms > Number of active Slidein forms' => $activeFormCounts[FormEntity::DISPLAY_TYPE_SLIDE_IN],
'Forms > Number of active Others (widget) forms' => $activeFormCounts[FormEntity::DISPLAY_TYPE_OTHERS],
'Forms > Number of active forms with first name' => $activeFormCounts['with_first_name'],
'Forms > Number of active forms with last name' => $activeFormCounts['with_last_name'],
'Forms > Number of active forms with custom fields' => $activeFormCounts['with_custom_fields'],
'Forms > Min custom fields' => $activeFormCounts['min_custom_fields'],
'Forms > Max custom fields' => $activeFormCounts['max_custom_fields'],
'Forms > Average custom fields' => $activeFormCounts['average_custom_fields'],
];
$result = array_merge(

View File

@@ -125,17 +125,18 @@ class FirstPurchase {
$meta = $queue->getMeta();
$result = (!empty($meta['order_date'])) ? WPFunctions::get()->dateI18n(get_option('date_format'), $meta['order_date']) : $defaultValue;
}
$this->loggerFactory->getLogger(self::SLUG)->info(
'handleOrderDateShortcode called',
[
'newsletter_id' => ($newsletter instanceof NewsletterEntity) ? $newsletter->getId() : null,
'subscriber_id' => ($subscriber instanceof SubscriberEntity) ? $subscriber->getId() : null,
'task_id' => ($queue instanceof SendingQueueEntity) ? (($task = $queue->getTask()) ? $task->getId() : null) : null,
'shortcode' => $shortcode,
'result' => $result,
]
);
}
$this->loggerFactory->getLogger(self::SLUG)->info(
'handleOrderDateShortcode called',
[
'newsletter_id' => ($newsletter instanceof NewsletterEntity) ? $newsletter->getId() : null,
'subscriber_id' => ($subscriber instanceof SubscriberEntity) ? $subscriber->getId() : null,
'task_id' => ($queue instanceof SendingQueueEntity) ? (($task = $queue->getTask()) ? $task->getId() : null) : null,
'shortcode' => $shortcode,
'result' => $result,
]
);
return $result;
}
@@ -149,17 +150,18 @@ class FirstPurchase {
$meta = $queue->getMeta();
$result = (!empty($meta['order_amount'])) ? $this->helper->wcPrice($meta['order_amount']) : $defaultValue;
}
$this->loggerFactory->getLogger(self::SLUG)->info(
'handleOrderTotalShortcode called',
[
'newsletter_id' => ($newsletter instanceof NewsletterEntity) ? $newsletter->getId() : null,
'subscriber_id' => ($subscriber instanceof SubscriberEntity) ? $subscriber->getId() : null,
'task_id' => ($queue instanceof SendingQueueEntity) ? (($task = $queue->getTask()) ? $task->getId() : null) : null,
'shortcode' => $shortcode,
'result' => $result,
]
);
}
$this->loggerFactory->getLogger(self::SLUG)->info(
'handleOrderTotalShortcode called',
[
'newsletter_id' => ($newsletter instanceof NewsletterEntity) ? $newsletter->getId() : null,
'subscriber_id' => ($subscriber instanceof SubscriberEntity) ? $subscriber->getId() : null,
'task_id' => ($queue instanceof SendingQueueEntity) ? (($task = $queue->getTask()) ? $task->getId() : null) : null,
'shortcode' => $shortcode,
'result' => $result,
]
);
return $result;
}

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