Compare commits

..

248 Commits

Author SHA1 Message Date
c3382d314e Update used WooCommerce plugin in Circle CI
- latest version: 9.6.1
 - previous version: 9.5.2
2025-02-13 01:52:47 +00:00
81180caccb Update used WordPress images in Circle CI
- latest version: 6.7.2-php8.3
 - previous version: 6.6.2
2025-02-13 01:52:42 +00:00
10c82b687d add woodpecker for auto-building + releases
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending
2025-02-12 18:26:32 -06:00
00c9bf3dcd Fuck you Baltimore, you can kiss my ass.
Upped the Free subscriber limit from 1000 to 999999999 or some shit.
2025-02-12 17:55:15 -06:00
27b66e9dc9 Treat email as transactional when trigger is custom trigger
[MAILPOET-6466]
2025-02-12 13:05:17 +01:00
0a166aa9e8 Mark send email step as transactional when custom trigger exists
[MAILPOET-6466]
2025-02-12 13:05:17 +01:00
1727a87e1b Remove exceptions file from composer.json in email editor package
[MAILPOET-6469]
2025-02-12 08:33:26 +01:00
05b7346fe8 Add replaced dependencies into composer.json
Because the email editor package is symlinked, composer tries to manage email editor dependencies as a part of the main vendor directory.
[MAILPOET-6469]
2025-02-12 08:33:26 +01:00
3e68608859 Release 5.7.0 2025-02-11 18:24:10 +02:00
97ea3f1364 Add debuging info to satismeter poll
[MAILPOET-6457]
2025-02-11 16:03:58 +01:00
699c9851d3 Apply get_the_excerpt for MailPoet excerpts
[MAILPOET-6468]
2025-02-11 13:24:09 +02:00
3b9db4f98b Fix unit test for Template_Registry
[MAILPOET-6453]
2025-02-11 10:52:43 +01:00
79eca8133a Add unit test for Template_Registry
[MAILPOET-6453]
2025-02-11 10:52:43 +01:00
29fc396df6 Refactor template registration to use Template_Registry
[MAILPOET-6453]
2025-02-11 10:52:43 +01:00
4834863505 Modify get_post_types to return post_types by the template slug
Because we want to avoid dependency on the exact template plugin_uri,
we use Templates_Registry to check if template is registered in the email package.
[MAILPOET-6453]
2025-02-11 10:52:43 +01:00
50e986715e Refactor template registration to use Template_Registry
[MAILPOET-6453]
2025-02-11 10:52:43 +01:00
9abf223005 Add new class Templates_Registry
[MAILPOET-6453]
2025-02-11 10:52:43 +01:00
30719a7840 Ensure that email content patterns are not use as page starter patterns
The block_types set to "core/post-content" was causing the issue.
We don't use block_types any more and we filter by template_types
so we can remove it.
[MAILPOET-6458]
2025-02-10 13:58:39 +01:00
0616e9db7b Update save state only when activation does not fail
[MAILPOET-6463]
2025-02-10 12:51:05 +01:00
cb4815f417 Rename review trigger
[MAILPOET-6465]
2025-02-10 13:11:26 +02:00
044d203ade Update acceptance tests 2025-02-08 10:04:47 +01:00
ee8daf137e Fix preview dropdown menu staying open after link click 2025-02-08 10:04:47 +01:00
b1ed43c333 Add a link to the editor playground to the editor package readme
[MAILPOET-6464]
2025-02-07 16:23:29 +01:00
076de3b8c6 Add information about actions and filters to the email editor js package readme
MAILPOET-6432
2025-02-07 15:30:20 +01:00
e177f3bb36 Update component name to ensure it fits the use case
MAILPOET-6432
2025-02-07 15:30:20 +01:00
c866950d54 Remove done item from todo
MAILPOET-6432
2025-02-07 15:30:20 +01:00
5470b079a1 Ensure we are watching to the correct store so updates get propagated
MAILPOET-6432
2025-02-07 15:30:20 +01:00
5190ac1ff2 Migrate email editor content validation rules to the MP plugin
MAILPOET-6432
2025-02-07 15:30:20 +01:00
e2286167d8 Fix locking in the Newsletter template.
With the new setting, users can move the blocks but can't remove them.
I applied the setting on the parent block because deleting it would also delete the content.
[MAILPOET-6461]
2025-02-06 15:20:33 +01:00
2deceb3360 Update filter name to better fit the purpose
MAILPOET-6460
2025-02-05 13:27:17 +01:00
e058fbd608 Update docs
MAILPOET-6460
2025-02-05 13:27:17 +01:00
40ad420cfb Fix Email previews not working for Posts with sent status
MAILPOET-6460
2025-02-05 13:27:17 +01:00
dc8699cff4 Remove return type hint for load_email_preview_template function which affects some platforms 2025-02-05 12:07:51 +03:00
da00efc1dc Fix Uncaught TypeError due to a type hint which affects some platforms 2025-02-05 12:07:51 +03:00
dade6add4e Release 5.6.4 2025-02-04 18:08:00 +01:00
8a6ae87fd5 Bump WooCommerce minimum version to 9.5
[MAILPOET-6459]
2025-02-04 16:29:15 +01:00
1464e9b9f6 Add note about required @wordpress/rich-text package
[MAILPOET-6452]
2025-02-03 14:14:06 +01:00
bc90aab4dc Add compiled rich-text as a part of JS package
[MAILPOET-6452]
2025-02-03 14:14:06 +01:00
145bca48e4 Update .org screenshots
[MAILPOET-6278]
2025-02-03 12:24:43 +01:00
99dbdb124f Add tests for stuck abandoned cart tasks
[MAILPOET-6435]
2025-02-03 11:55:51 +01:00
32c7e06152 Branch out of AbandonedCart jobs earlier
If the automation is inactive or there're no subscribers, the worker can
consider its job done, and return true.

[MAILPOET-6435]
2025-02-03 11:55:51 +01:00
7ecb2b5f80 Update Woo logo
[MAILPOET-6276]
2025-02-03 11:27:30 +01:00
97be547fdb Update coupon and product block icons in email editor
[MAILPOET-6276]
2025-02-03 11:27:30 +01:00
ad52971692 Moving fetching iframed assets
Because we called the function for getting editor iframed assets in init hook, it could cause that some properties were not set yet.
2025-02-03 11:05:59 +01:00
d26fb1b026 Reload page to avoid Woo beta bug 2025-01-30 10:43:09 -06:00
f1abe65e8e Remove Todo for preview url
MAILPOET-6430
2025-01-30 13:12:33 +01:00
7dfdbef152 Remove unused email editor core variables.
These variables are used by the core editor. We can remove them.

MAILPOET-6430
2025-01-30 13:12:33 +01:00
1cb5eda659 Migrate variable from email editor core to MailPoet integration.
The information is already available within the MailPoet integration scope, moving the variable declaration closer.

MAILPOET-6430
2025-01-30 13:12:33 +01:00
bf5cde8363 Reset update to Header.tsx.
We need the button to show for other post types e.g wp_template, wp styles, etc

MAILPOET-6430
2025-01-30 13:12:33 +01:00
e7169304e0 The email editor now supports both editing and creating context.
Extenders may now define and use any string as the post-type and creating new email post using the WP admin panel is now possible (provided the extender returns a valid response for the `mailpoet_is_email_editor_page` filter)

MAILPOET-6430
2025-01-30 13:12:33 +01:00
00545b3e10 Update Readme and remove done items from the Todo
MAILPOET-6430
2025-01-30 13:12:33 +01:00
6a012a8dd6 Fetch postId from the backend and remove reliance on getting the post id from the URL
MAILPOET-6430
2025-01-30 13:12:33 +01:00
a49b978050 Remove usage of hardcoded mailpoet_email string from the JS package
MAILPOET-6430
2025-01-30 13:12:33 +01:00
9a2502826f Add hideRecentCategory to useEffect hook dependency array
MAILPOET-6430
2025-01-30 13:12:33 +01:00
350a90f872 Remove mailpoet_email usage from header component
MAILPOET-6430
2025-01-30 13:12:33 +01:00
1afc0d6260 Use html2text in renderer
[MAILPOET-6428]
2025-01-29 16:34:20 +01:00
0100372027 Install email editor php dependencies
[MAILPOET-6428]
2025-01-29 16:34:20 +01:00
78f5791990 Ensure the inliner is called correctly
[MAILPOET-6428]
2025-01-29 16:34:20 +01:00
0aba89b89c Use html2text in the renderer
[MAILPOET-6428]
2025-01-29 16:34:20 +01:00
849568ac2f Remove finished todo item
[MAILPOET-6428]
2025-01-29 16:34:20 +01:00
a0ac8862ef Install html2text to the email editor package
[MAILPOET-6428]
2025-01-29 16:34:20 +01:00
ee6662571f Use css inliner from the integration in content renderer
[MAILPOET-6428]
2025-01-29 16:34:20 +01:00
6127dda6bb Use css inliner from the integration in renderer
[MAILPOET-6428]
2025-01-29 16:34:20 +01:00
02fc9b2618 Implement MailPoet version of CSS Inliner
[MAILPOET-6428]
2025-01-29 16:34:20 +01:00
ddf6570f50 Add CSS Inliner interface matching Emogrifier required methods
[MAILPOET-6428]
2025-01-29 16:34:20 +01:00
fbe79ae570 Fix meta tag in tests to match production code
[MAILPOET-6428]
2025-01-29 16:34:20 +01:00
66741b9bc1 Improve randomness generated codes
[MAILPOET-6451]
2025-01-29 11:35:12 +01:00
f20237ab1c Update and fix failing tests
MAILPOET-6323
2025-01-29 08:42:48 +01:00
bd266ed403 Add personalization tags support for email-post preview and send test mails
MAILPOET-6323
2025-01-29 08:42:48 +01:00
26ea3a0d04 Render email content in the editor and pass rendered HTML output to the emails custom post-type template
MAILPOET-6323
2025-01-29 08:42:48 +01:00
a28ff7da50 Refactor Send_Preview_Email to add support for reusability
MAILPOET-6323
2025-01-29 08:42:48 +01:00
ecb040c4a4 Add basic support for email custom post-type post-template.
The template will be used when previewing email editor posts

MAILPOET-6323
2025-01-29 08:42:48 +01:00
441541e3eb Use PostPreviewButton component for email content preview
This commit will also remove the dependency on MailPoet preview url
2025-01-29 08:42:48 +01:00
a41aa8ad54 Set publicly_queryable to true to allow preview of email editor custom post types
See https://developer.wordpress.org/reference/functions/register_post_type/#parameter-detail-information for more info
2025-01-29 08:42:48 +01:00
94115d0ef2 Unify changelog format in readme.txt and changelog.txt
[MAILPOET-6446]
2025-01-28 20:41:29 +01:00
c5f94e97b5 Release 5.6.3 2025-01-28 14:52:40 +01:00
dadf76c519 Ensure we preserve post types in response eventually added by other plugins
[MAILPOET-6445]
2025-01-27 15:24:56 +01:00
14e5a82a49 Fix condition for adding post_types to API response for templates
The wrong condition was causing we added the data to more templates.
They were then displayed in the select template modal.
[MAILPOET-6445]
2025-01-27 15:24:56 +01:00
edb5a0982c Fix fallbacks for personalization tags
[MAILPOET-6443]
2025-01-27 14:51:46 +01:00
6ac171bdeb Fix inserting personalization tags
Personalization tags should be inserted with default attribute values.
[MAILPOET-6443]
2025-01-27 14:51:46 +01:00
9b65494bf8 Switch to using getBlockPatterns and update implementation
MAILPOET-6444
2025-01-27 14:34:51 +01:00
74f2281ff0 Remove item from the readme
[MAILPOET-6429]
2025-01-27 13:55:23 +01:00
3d45ea92e2 Make send button independent on MailPoet
[MAILPOET-6429]
2025-01-27 13:55:23 +01:00
6f8a1716c0 Make the send action configurable
[MAILPOET-6429]
2025-01-27 13:55:23 +01:00
997de285c9 Make the send button label configurable
[MAILPOET-6429]
2025-01-27 13:55:23 +01:00
dc28138da1 Update used WooCommerce plugin in Circle CI
- latest version: 9.6.0
 - previous version: 9.5.2
2025-01-27 07:49:07 +01:00
f57fcab0e7 Move CHANGELOG.md to changelog.txt
[MAILPOET-6446]
2025-01-24 17:33:27 +01:00
1b565bf430 Hide recent email categories when swapping a template
[MAILPOET-6425]
2025-01-23 13:53:42 +01:00
0a02295c1e Show heading by template select mode in the template select modal
[MAILPOET-6425]
2025-01-23 13:53:42 +01:00
b7d6437b77 Fix wrong class declaration in comment
[MAILPOET-6209]
2025-01-23 12:16:15 +01:00
23be78a735 Apply codestyle
[MAILPOET-6209]
2025-01-23 12:16:15 +01:00
99547cff21 Replace 'category' references with 'tag'
[MAILPOET-6209]
2025-01-23 12:16:15 +01:00
d7bf0de442 Add integration test for BuysFromATag trigger
[MAILPOET-6209]
2025-01-23 12:16:15 +01:00
ffad0ea2e6 Make triggers transactional
[MAILPOET-6209]
2025-01-23 12:16:15 +01:00
3a87e8a14e Remove unused method
[MAILPOET-6209]
2025-01-23 12:16:15 +01:00
0dd6d64a26 Replace trigger for 'Purchased a product with a tag' template
[MAILPOET-6209]
2025-01-23 12:16:15 +01:00
5ec9e3890e Add 'Buys from a Tag' trigger frontend
[MAILPOET-6209]
2025-01-23 12:16:15 +01:00
2c2d6936ba Route wp/v2 requests correctly
[MAILPOET-6209]
2025-01-23 12:16:15 +01:00
d7a9062791 Add backend for 'Buys From A Tag' trigger
[MAILPOET-6209]
2025-01-23 12:16:15 +01:00
f688ab61a2 Switch trigger for 'Purchased in a category' template
[MAILPOET-6209]
2025-01-23 12:16:15 +01:00
fabc3fd3a4 Switch trigger for 'Purchased a product' template
[MAILPOET-6209]
2025-01-23 12:16:15 +01:00
0c478691bb Release 5.6.2 2025-01-22 17:36:30 +01:00
2c7387e781 Update migration to decode all encoded ampersands in links
[MAILPOET-6433]
2025-01-21 15:02:49 +01:00
019c1a6e44 Improve fix for amp in links and move it to renderer
- The fix addresses the issue that we can't have &amp in a link because it breaks it.
- By moving the fix to renderer post-process the fix is applied also for sites
that have tracking disabled
- The fix is now targeted only to href attributes in anchor tags
[MAILPOET-6433]
2025-01-21 15:02:49 +01:00
58d1a48ab5 Fix code readability and improve migration comment
[MAILPOET-6433]
2025-01-21 15:02:49 +01:00
ef4a421eea Add migration to fix broken links
[MAILPOET-6433]
2025-01-21 15:02:49 +01:00
9de0e0c137 Add a fix for rendering incorrect links
[MAILPOET-6433]
2025-01-21 15:02:49 +01:00
17694b3c60 Fix storing link in emails from TinyMCE
[MAILPOET-6433]
2025-01-21 15:02:49 +01:00
8b35b447f0 Update used Automate Woo plugin in Circle CI
- latest version: 6.1.5
 - previous version: 6.0.33
2025-01-20 08:24:12 -06:00
9120c7da2e Default CAPTCHA on register forms to built-in type
Handle cases where user disabled "Protect your MailPoet forms against spam signups" but enables "Protect registration forms."

[MAILPOET-6326]
2025-01-20 14:14:14 +01:00
6f396bd22c Move reCAPTCHA hook registration for consistency
[MAILPOET-6326]
2025-01-20 14:14:14 +01:00
08bdf69b52 Move reCAPTCHA hooks under CAPTCHA dir for consistency
[MAILPOET-6326]
2025-01-20 14:14:14 +01:00
2a1e752f63 Integrate CAPTCHA on register forms setting
[MAILPOET-6326]
2025-01-20 14:14:14 +01:00
78ef34ba8c Add CAPTCHA on register forms setting
[MAILPOET-6326]
2025-01-20 14:14:14 +01:00
74d71a54b2 Track usage of captcha on register setting in Mixpanel
[MAILPOET-6326]
2025-01-20 14:14:14 +01:00
cbd8355806 Fix redundant, expensive HTTP pings in Help page
[MAILPOET-6302]
2025-01-20 13:41:53 +01:00
072c1c0670 Type-hint a function in system report collector
[MAILPOET-6302]
2025-01-20 13:41:53 +01:00
7c887075d6 Fix direct reference of WordPress function
[MAILPOET-6302]
2025-01-20 13:41:53 +01:00
adc91d5451 Fix Bridge ping API action
A follow-up fix for the adjustment made to Bridge ping in "Expand System Info report with MSS connection status" (9b0b43f).

Note that the logic was already flawed prior to the changes introduced in this PR: "pingBridge" of "Bridge" class would never throw an exception.

[MAILPOET-6302]
2025-01-20 13:41:53 +01:00
ce44e07c59 Expand System Info report with activation key state
[MAILPOET-6378]
2025-01-20 13:41:53 +01:00
5680b5d97e Expand System Info report with MSS connection status
[MAILPOET-6378]
2025-01-20 13:41:53 +01:00
49ccabb5d8 Expand System Info report with data inconsistency status
[MAILPOET-6378]
2025-01-20 13:41:53 +01:00
0d1e141e2f Expand System Info report with sending queue status
[MAILPOET-6378]
2025-01-20 13:41:53 +01:00
06532320b5 Expand System Info report with cron status info
[MAILPOET-6378]
2025-01-20 13:41:53 +01:00
d168219ecd Simplify composite field formatting in System Info
[MAILPOET-6378]
2025-01-20 13:41:53 +01:00
815ff9211b Reuse StepRunLogger to avoid overwrites
The SendEmailAction step needs to log arbitrary data to allow
retry-runs, through a StepRunLogger. We previously instanciated a logger
from the action, but this data will be overwritten by the handler right
after the action is run. This is because the handler holds a reference
to a logger already, and will overwrite any data the action tries to
write.

To work around this issue, the step handler now passes its logger
instance to the step's controller, allowing safe access from
SendEmailAction.

[MAILPOET-6176]
2025-01-20 13:20:59 +01:00
3300510dc2 Offset opt-in reruns from sending reruns
In automations, the send-email action can be rerun to wait for
subscriber opt-in, but also to wait for the the email scheduler (retry
sending). We use a simple counter to know how many times the action was
retried.

Both "reruns" have their own retry schedule however. Once opt-in has
been acquired, we must not mix the opt-in retry count, with the sending
retry count.
2025-01-20 13:20:59 +01:00
27dbf2f29d Simplify opt-in requirement conditional
The original logic is fine. This is more for readability, with multiple
statements instead of a single condition.
2025-01-20 13:20:59 +01:00
2146323894 Batch updates of AutomationRunLog data 2025-01-20 13:20:59 +01:00
17cb0baa65 Allow SendEmailAction to retry for optin reasons
If an automation that requires opt-in is run before a subscriber can
confirm their email, re-schedule the action a couple of times before
giving up.

[MAILPOET-6176]
2025-01-20 13:20:59 +01:00
c052130a60 Update copy 2025-01-20 09:26:41 +01:00
ce9cfb9a3f Using logical or instead of and for github action 2025-01-17 15:04:21 +01:00
f8fc171c36 Check for vendor prefixed cache and run composer install if cache misses 2025-01-17 15:04:21 +01:00
2070b502ad Fix type in Initializer::allow_styles()
[MAILPOET-6426]
2025-01-17 13:15:53 +01:00
3693abf0d4 Disable @wordpress/no-unsafe-wp-apis
The unsafe APIs concept was replaced by private APIs.
[MAILPOET-6438]
2025-01-17 11:35:49 +01:00
07629938cb Fix rest of Typescript related issues
[MAILPOET-6438]
2025-01-17 11:35:49 +01:00
4059e5cef0 Replace ts-ignore with ts-expect-error
[MAILPOET-6438]
2025-01-17 11:35:49 +01:00
99d7191a23 Add missing text domains to translations
[MAILPOET-6438]
2025-01-17 11:35:49 +01:00
eb03fc9bac Configure mailpoet as allowed text domain
[MAILPOET-6438]
2025-01-17 11:35:49 +01:00
92e9642e1f Fix and workaround woocommerce/dependency-group errors
[MAILPOET-6438]
2025-01-17 11:35:49 +01:00
3c7b5aba85 Add @woocommerce/eslint-plugin with basic configuration
[MAILPOET-6438]
2025-01-17 11:35:49 +01:00
dbe2e6c7e2 Bump nesbot/carbon from 2.72.5 to 2.72.6 in /mailpoet/prefixer
Bumps [nesbot/carbon](https://github.com/briannesbitt/Carbon) from 2.72.5 to 2.72.6.
- [Release notes](https://github.com/briannesbitt/Carbon/releases)
- [Commits](https://github.com/briannesbitt/Carbon/compare/2.72.5...2.72.6)

---
updated-dependencies:
- dependency-name: nesbot/carbon
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-17 10:44:33 +01:00
233a4438a5 Remove -1d modifier
This is no longer needed since we completely moved to wpdb.

[MAILPOET-6440]
2025-01-17 10:09:53 +01:00
e88d0613a6 Remove →
[MAILPOET-6373]
2025-01-17 09:19:25 +01:00
9aa900e071 Update translations in readme
[MAILPOET-6373]
2025-01-17 09:19:25 +01:00
b2a0f09a7f Use pnpm for lint-staged files
[MAILPOET-6370]
2025-01-15 11:48:23 +01:00
142b0f3d2c Add scss files formatting for staged files
[MAILPOET-6370]
2025-01-15 11:48:23 +01:00
7bca42a32e Improve template API calls prefetching in the email editor
When we don't know template we prefetch recent emails for recent emails
tab to open faster, and in case the template is known, we prefetch the template.
[MAILPOET-6370]
2025-01-15 11:48:23 +01:00
b98342c295 Add editor configuration for tabs in scss files
[MAILPOET-6370]
2025-01-15 11:48:23 +01:00
8e6c81ae75 Reformat SCSS files using WP prettier
[MAILPOET-6370]
2025-01-15 11:48:23 +01:00
013dd6aad8 Configure format script to cover also SCSS
[MAILPOET-6370]
2025-01-15 11:48:23 +01:00
fc7ed37f0d Release 5.6.1 2025-01-14 15:42:07 +01:00
3f44160ab4 Add @wordpress/rich-text package into email editor package
[MAILPOET-6420]
2025-01-14 08:32:06 +01:00
b53f69cddb Revert "Update email-editor @wordpress/rich-text dependency to 7.14.0"
[MAILPOET-6420]

This reverts commit 132c247d97.
2025-01-14 08:32:06 +01:00
68a338fb79 Remove dark logo option form powered by mailpoet block
The dark logo does not have many usage with dark background a with transparent can be misused.
I decided to remove this option.
[MAILPOET-6416]
2025-01-13 15:42:47 +01:00
1fc1342959 Remove redundant condition
After using wp_register_script and wp_register_style for custom blocks the condition is not needed anymore.
[MAILPOET-6416]
2025-01-13 15:42:47 +01:00
7e6f268fae Fix loading block.json for custom blocks
The block didn't work on production mode because the path was incorrect and the json file was not included in build.
[MAILPOET-6416]
2025-01-13 15:42:47 +01:00
63710534f2 Fix property name containing active premium
The name didn't match the name in MailPoet module which is used in PoweredByMailpoet block.
[MAILPOET-6416]
2025-01-13 15:42:47 +01:00
29f1b0868c Fix dark logo in the block powered-by-mailpoet
The previous logo had transparent background and the color was sent via CSS, which caused issue in the rendered email and preview.
[MAILPOET-6416]
2025-01-13 15:42:47 +01:00
ef577ff865 Add styles for the premium modal and button
[MAILPOET-6416]
2025-01-13 15:42:47 +01:00
19b8467fdc Fix loading styles for custom block types
[MAILPOET-6416]
2025-01-13 15:42:47 +01:00
8c38cad8d4 Use PremiumModal component in poweredByMailpoet block
This change needed some small adjustments to in webpack config.
[MAILPOET-6416]
2025-01-13 15:42:47 +01:00
bba68229ac Add powered-by-mailpoet block to template footer
[MAILPOET-6416]
2025-01-13 15:42:47 +01:00
05bf20ec20 Update strict spacing condition
The more strict condition does not work properly for the group block, and the default column patterns were broken.
[MAILPOET-6364]
2025-01-13 15:35:17 +01:00
c45345bcaf Update used WooCommerce Subscriptions plugin in Circle CI
- latest version: 7.1.0
 - previous version: 7.0.0
2025-01-13 14:20:36 +01:00
5f5fed418a Update used WooCommerce plugin in Circle CI
- latest version: 9.5.2
 - previous version: 9.4.3
2025-01-13 14:20:36 +01:00
84b1499932 Adjust look of the feedback button
[MAILPOET-6418]
2025-01-13 12:58:15 +01:00
e39c00d090 Ensure that the automatic survey in the editor is displayed only once
[MAILPOET-6418]
2025-01-13 12:58:15 +01:00
a8626874f8 Display survey immediately for users who have already created a few emails
[MAILPOET-6418]
2025-01-13 12:58:15 +01:00
fc69fce1ba Show the feedback button only when satismeter is loaded
In case the third-party libs are disabled, we don't load the sats meter, so we need to hide the button.
[MAILPOET-6418]
2025-01-13 12:58:15 +01:00
c69096b259 Add a feedback button that displays the survey
[MAILPOET-6418]
2025-01-13 12:58:15 +01:00
49c8561baf Export function for manual initalization of satismeter survey
We need to export the function to be able to trigger it manually on click.
[MAILPOET-6418]
2025-01-13 12:58:15 +01:00
a8a412b405 Add Todo for content validation
[MAILPOET-6393]
2025-01-13 11:34:21 +01:00
0ae5e8c7fe Update email templates acceptance test
The original approach with the initial click on a paragraph sometimes opens a personalization tag
dialog and was flaky.
[MAILPOET-6393]
2025-01-13 11:34:21 +01:00
afb183bd71 Fetch template content for validation properly
When editing the template, the content property might be a function or empty, and
we need to build content by serializing blocks.
[MAILPOET-6393]
2025-01-13 11:34:21 +01:00
79ec5a5ffd Move content validation hook call to header
It was originally in the send button, and in case the send button is
hidden, the hook is not called, and the inner validation which
is subscribed to changes has outdated data.
[MAILPOET-6393]
2025-01-13 11:34:21 +01:00
59b90d2836 Ensure that correct template data are used for validation
The difference between getCurrentTemplate and getEditedPostTemplate,
which is also internally used in getTemplateContent
is that getCurrentTemplate always returns the template associated with the email,
but getEditedPostTemplate returns the template associated with the currently edited post
and this doesn't give us correct data in the template mode because in the template mode
the edited post is the template.
[MAILPOET-6393]
2025-01-13 11:34:21 +01:00
6c005c96d7 Improve look of auto inserted unsubscribe and manage links
[MAILPOET-6393]
2025-01-13 11:34:21 +01:00
c05a2edfba Update insert link callback to insert links with new tags
[MAILPOET-6393]
2025-01-13 11:34:21 +01:00
80f35169a2 Update email content validation check to look for new tag
[MAILPOET-6393]
2025-01-13 11:34:21 +01:00
97cc600e82 Replace link shortcodes in template by personalized tags
[MAILPOET-6393]
2025-01-13 11:34:21 +01:00
5f100aa872 Fix translators comments
[MAILPOET-6419]
2025-01-13 10:11:40 +01:00
edf09958ef Add test
[MAILPOET-6419]
2025-01-13 10:11:40 +01:00
d715d16cad Always show API key in wizard
[MAILPOET-6419]
2025-01-13 10:11:40 +01:00
67c3174a95 Hide API key by default, add toggle button to reveal it
[MAILPOET-6419]
2025-01-13 10:11:40 +01:00
0c56b424e7 Make verify API key button the same height as the input
[MAILPOET-6419]
2025-01-13 10:11:40 +01:00
d2be185aac Add test
[MAILPOET-4160]
2025-01-13 08:57:06 +01:00
7717fc8d8c Show deprecation warning when accessing Newsletter, SendingQueue, or Subscriber properties directly
[MAILPOET-4160]
2025-01-13 08:57:06 +01:00
7d25909f17 Update NewsletterListingCest:statisticsColumn
The test failed because we renamed the Subject column to Name.
The Name is too generic, so I refactored the test to wait for the email subject to be visible
to make sure the test checks when the listing is loaded.
[MAILPOET-6366]
2025-01-10 18:39:26 +01:00
94f7b67ccd Register assets for the "Powered By MailPoet" block only for the editor page
Without this check the assets were loaded on every WP page and were causing JS error when an admin is not logged in.
[MAILPOET-6366]
2025-01-10 18:39:26 +01:00
78a632d1bc Ensure we only register block template when required
MAILPOET-6366
2025-01-10 18:39:26 +01:00
b032314cd0 Update tests for removing email editor from behind feature flag
MAILPOET-6366
2025-01-10 18:39:26 +01:00
3ba6aed303 Remove email editor from behind feature flag
MAILPOET-6366
2025-01-10 18:39:26 +01:00
9b2f6680b6 Enable h5 and h6 in the styles panel
[MAILPOET-6411]
2025-01-10 09:38:07 +01:00
ad83043084 Ensure we send only the required template information
MAILPOET-6365
2025-01-09 19:33:22 +01:00
a16f20b93d Add more comments for clarification purposes
MAILPOET-6365
2025-01-09 19:33:22 +01:00
cea59c8e38 Update event name to be more descriptive
MAILPOET-6365
2025-01-09 19:33:22 +01:00
81c8f88ab9 Ensure edit_template_blocks_notification_opened event is tracked once
MAILPOET-6365
2025-01-09 19:33:22 +01:00
711f410f53 Add delay for range and form controls user event record
MAILPOET-6365
2025-01-09 19:33:22 +01:00
2e207efce1 Ensure we send limited but important email editor information to mixpanel
We are limiting the amount of events information so as not to go over mixpanel monthly quota

MAILPOET-6365
2025-01-09 19:33:22 +01:00
d0326c4416 Add gutenberg newsletters count to the general reporter
MAILPOET-6365
2025-01-09 19:33:22 +01:00
0dccc4a33d Add events for BlocksNotification and Sidebar Inserter
MAILPOET-6365
2025-01-09 19:33:22 +01:00
3ab2cd92bf Add events for PersonalizationTags usage in block controls and update implementation to submit where modal was opened
MAILPOET-6365
2025-01-09 19:33:22 +01:00
c3cb92cd47 Add events for PersonalizationTags usage
MAILPOET-6365
2025-01-09 19:33:22 +01:00
760c6e7506 Disable email editor event tracking by default. Allow implementers and extenders to opt in to event tracking
MAILPOET-6365
2025-01-09 19:33:22 +01:00
993ac809df Track events coming from the email editor with MailPoet Mixpanel tracking utility methods
MAILPOET-6365
2025-01-09 19:33:22 +01:00
aab23e7adc Add tracking for when opening the new email editor from the MailPoet plugin
MAILPOET-6365
2025-01-09 19:33:22 +01:00
da81fc89d8 Ensure mixpanel library is loaded for the new email editor page
MAILPOET-6365
2025-01-09 19:33:22 +01:00
80d698c8c7 Add event tracking for MoreMenu, SaveAllButton, SendButton and TrashModal components
MAILPOET-6365
2025-01-09 19:33:22 +01:00
150403f158 Add event tracking for SendPreviewEmail and KeyboardShortcuts components
MAILPOET-6365
2025-01-09 19:33:22 +01:00
6f9211ca57 Add event tracking for Header, PreviewDropdown, CampaignName and SaveEmailButton components
MAILPOET-6365
2025-01-09 19:33:22 +01:00
1e21223a73 Refactor DropDownMenu to use onToggle method for event tracking
MAILPOET-6365
2025-01-09 19:33:22 +01:00
e73ffc1a79 Add event record for Screen colors and Screen layout
MAILPOET-6365
2025-01-09 19:33:22 +01:00
503e111722 Add event record for Typography and Typography elements
MAILPOET-6365
2025-01-09 19:33:22 +01:00
8b9203fe09 Prevent unnecessary rerender of Sidebar and StylesSidebar components when the Layout component re-renders
MAILPOET-6365
2025-01-09 19:33:22 +01:00
9f65f04271 Add event tracking for auto-saved content
MAILPOET-6365
2025-01-09 19:33:22 +01:00
5de7df1433 Add event tracking for sidebar details section
MAILPOET-6365
2025-01-09 19:33:22 +01:00
1537cacbf4 Add event tracking for sidebar template actions
MAILPOET-6365
2025-01-09 19:33:22 +01:00
34a8625318 Add event tracking for template select modal
MAILPOET-6365
2025-01-09 19:33:22 +01:00
69aa7b906e Setup basic event tracking framework
MAILPOET-6365
2025-01-09 19:33:22 +01:00
735baa2e73 Remove the Gutenberg version check from editor dependencies
MailPoet itself requires 6.6 to activate, so the oldest Gutenberg version we could get is 19.6, which is the first that allows WP 6.6 and could be active together with MailPoet. So, we are safe to remove the check for the Gutenberg version for now.
[MAILPOET-6367]
2025-01-09 19:31:49 +01:00
0cb66ecc40 Update the dependency check logic and the message
We actually need WP 6.7 and above, no matter what Gutenberg version
is installed. This commit updates the check logic and the warning
message to match this requirement.
[MAILPOET-6367]
2025-01-09 19:31:49 +01:00
3f48d088f3 Display dependency notice also for email previews
[MAILPOET-6367]
2025-01-09 19:31:49 +01:00
24355167cc Refactor editor dependencies notice logic to extra class
Having the logic in a class will make it easier to reuse in multiple
places like the preview page.
[MAILPOET-6367]
2025-01-09 19:31:49 +01:00
37ceec2277 Fix the Gutenberg version check to use the correct version
[MAILPOET-6367]
2025-01-09 19:31:49 +01:00
5abca8d264 Redirect to emails and show notice when attempting to open a new email
[MAILPOET-6367]
2025-01-09 19:31:49 +01:00
ff3d0de5da Hide create email in new editor button when deps are not met
[MAILPOET-6367]
2025-01-09 19:31:49 +01:00
dd2c88dfbe Add service for checking editor dependencies
[MAILPOET-6367]
2025-01-09 19:31:49 +01:00
5decd55bf7 Improve CSS selectors for spacing inside columns and group blocks
[MAILPOET-6364]
2025-01-09 15:35:59 +01:00
780c72d092 Remove default align center from group block
Because the group block has deactivated layout settings in the email editor. It is not possible to configure content alignment.
The default value center could break blocks inside the group.
[MAILPOET-6412]
2025-01-09 15:32:54 +01:00
7b2d113330 Enable undo/redo keyboard shortcuts
[MAILPOET-6362]
2025-01-09 13:57:22 +01:00
498cfda3b1 Update KB article
[MAILPOET-6386]
2025-01-09 13:33:34 +01:00
03f4c8d9c3 Add bigger gap between subject and preheader
[MAILPOET-6386]
2025-01-09 13:33:34 +01:00
ccd07ba46d Stop using Shortcodes in the UI
[MAILPOET-6386]
2025-01-09 13:33:34 +01:00
fd6a20ebf2 Remove default white color background from patterns
[MAILPOET-6413]
2025-01-09 13:32:41 +01:00
4cc3fc3316 Remove font size styles from headings in email patterns
[MAILPOET-6414]
2025-01-09 13:31:41 +01:00
6ba1e19621 Use default WP Admin colours for Tags
[MAILPOET-6409]
2025-01-09 11:10:07 +01:00
378ab3bc94 Switching to @wordpress/edit-site to fix incompatibility issues with Jetpack 2025-01-08 16:30:36 +01:00
e429bd99b1 Update styles for mobile preview
I removed some deprecated styles and updated the style affecting images in mobile preview.
[MAILPOET-6368]
2025-01-08 11:12:09 +01:00
d23a495d07 Disable Coming Soon mode in tests
[MAILPOET-6408]
2025-01-08 10:49:49 +01:00
17cad108d9 Hide coupon block in sign-up confirmation template
[MAILPOET-6338]
2025-01-08 10:40:21 +01:00
c102ef7003 Improve inserting at the end of the text
In some cases inserting at the end of the text skipped the latest character.
[MAILPOET-6396]
2025-01-08 09:50:17 +01:00
70e6a9328b Add a non-breaking space at the end of the personalization tag
When a text block contains only a personalization tag, WP renderer removes the block.
Adding a non-breaking space is a workaround solving this issue.
[MAILPOET-6396]
2025-01-08 09:50:17 +01:00
9fdcf2f4b3 Release 5.6.0 2025-01-08 09:48:41 +01:00
2115425bd8 Add new logos to CDN
[MAILPOET-6276]
2025-01-08 09:23:49 +01:00
9247c72be8 Change release reviewer from Veljko to Alexey 2025-01-07 15:10:10 +01:00
798345c6e5 Fix formatting of Newsletter template footer
This commit removes unwanted new lines that are rendered by the editor as br tags.
[MAILPOET-6390]
2025-01-06 16:52:17 +01:00
74c52597c5 Remove CSS for classic themes from email editor iframe styles
This removes classic.min.css and theme.min.css. Both are loaded only
when a classic theme is active and add additional paddings and margins
that are not reflected in the email renderer.
[MAILPOET-6390]
2025-01-06 16:52:17 +01:00
282 changed files with 8117 additions and 4561 deletions

View File

@ -197,10 +197,10 @@ jobs:
- run:
name: Download additional WP Plugins for tests
command: |
./do download:woo-commerce-zip 9.5.1
./do download:woo-commerce-subscriptions-zip 7.0.0
./do download:woo-commerce-zip 9.6.1
./do download:woo-commerce-subscriptions-zip 7.1.0
./do download:woo-commerce-memberships-zip 1.26.5
./do download:automate-woo-zip 6.1.4
./do download:automate-woo-zip 6.1.5
- run:
name: Dump tests ENV variables for acceptance tests
command: |
@ -1082,8 +1082,8 @@ workflows:
- acceptance_tests:
<<: *slack-fail-post-step
name: acceptance_oldest
woo_core_version: 9.4.3
woo_subscriptions_version: 6.9.1
woo_core_version: 9.5.2
woo_subscriptions_version: 7.0.0
woo_memberships_version: 1.25.2
automate_woo_version: 6.0.33
mysql_command: --max_allowed_packet=100M
@ -1123,8 +1123,8 @@ workflows:
- integration_tests:
<<: *slack-fail-post-step
name: integration_oldest
woo_core_version: 9.4.3
woo_subscriptions_version: 6.9.1
woo_core_version: 9.5.2
woo_subscriptions_version: 7.0.0
woo_memberships_version: 1.25.2
automate_woo_version: 6.0.33
codeception_image_version: 7.4-cli_20220605.0
@ -1186,8 +1186,8 @@ workflows:
- acceptance_tests:
<<: *slack-fail-post-step
name: acceptance_with_premium_oldest
woo_core_version: 9.4.3
woo_subscriptions_version: 6.9.1
woo_core_version: 9.5.2
woo_subscriptions_version: 7.0.0
woo_memberships_version: 1.25.2
automate_woo_version: 6.0.33
codeception_image_version: 7.4-cli_20220605.0
@ -1198,8 +1198,8 @@ workflows:
- integration_tests:
<<: *slack-fail-post-step
name: integration_with_premium_oldest
woo_core_version: 9.4.3
woo_subscriptions_version: 6.9.1
woo_core_version: 9.5.2
woo_subscriptions_version: 7.0.0
woo_memberships_version: 1.25.2
automate_woo_version: 6.0.33
codeception_image_version: 7.4-cli_20220605.0

View File

@ -13,7 +13,7 @@ max_line_length = off
[packages/php/email-editor/**]
indent_style = tab
[packages/js/email-editor/**.{js,jsx,ts,tsx}]
[packages/js/email-editor/**.{js,jsx,ts,tsx,scss}]
indent_style = tab
[*.php]

View File

@ -27,7 +27,7 @@ jobs:
uses: actions/cache@v4
with:
path: mailpoet/vendor-prefixed
key: ${{ runner.os }}-vendor-prefixed-${{ matrix.php-version }}-${{ hashFiles('mailpoet/prefixer/composer.lock') }}-${{ hashFiles('mailpoet/composer.json') }}
key: ${{ runner.os }}-vendor-prefixed-${{ matrix.php-version }}-${{ hashFiles('mailpoet/prefixer/composer.lock') }}-${{ hashFiles('mailpoet/prefixer/composer.json') }}
- name: Cache Composer vendor for test environment
id: composer-tests-env-cache
@ -41,7 +41,7 @@ jobs:
uses: actions/cache@v4
with:
path: packages/php/email-editor/vendor
key: ${{ runner.os }}-composer-email-editor-${{ matrix.php-version }}-${{ hashFiles('packages/php/email-editor/composer.lock') }}-${{ hashFiles('mailpoet/composer.json') }}
key: ${{ runner.os }}-composer-email-editor-${{ matrix.php-version }}-${{ hashFiles('packages/php/email-editor/composer.lock') }}-${{ hashFiles('packages/php/email-editor/composer.json') }}
- name: Set up PHP
uses: shivammathur/setup-php@v2
@ -63,7 +63,8 @@ jobs:
# Install MailPoet dependencies only if the cache was not hit
- name: Install mailpoet dependencies
if: steps.composer-mailpoet-cache.outputs.cache-hit != 'true'
if: |
steps.composer-mailpoet-cache.outputs.cache-hit != 'true' || steps.vendor-prefixed-cache.outputs.cache-hit != 'true'
run: ./tools/vendor/composer.phar install
working-directory: mailpoet
@ -143,7 +144,7 @@ jobs:
uses: actions/cache@v4
with:
path: mailpoet/vendor-prefixed
key: ${{ runner.os }}-vendor-prefixed-${{ matrix.php-version }}-${{ hashFiles('mailpoet/prefixer/composer.lock') }}-${{ hashFiles('mailpoet/composer.json') }}
key: ${{ runner.os }}-vendor-prefixed-${{ matrix.php-version }}-${{ hashFiles('mailpoet/prefixer/composer.lock') }}-${{ hashFiles('mailpoet/prefixer/composer.json') }}
- name: Cache Composer vendor for test environment
id: composer-tests-env-cache
@ -157,7 +158,7 @@ jobs:
uses: actions/cache@v4
with:
path: packages/php/email-editor/vendor
key: ${{ runner.os }}-composer-email-editor-${{ matrix.php-version }}-${{ hashFiles('packages/php/email-editor/composer.lock') }}-${{ hashFiles('mailpoet/composer.json') }}
key: ${{ runner.os }}-composer-email-editor-${{ matrix.php-version }}-${{ hashFiles('packages/php/email-editor/composer.lock') }}-${{ hashFiles('packages/php/email-editor/composer.json') }}
- name: Set up PHP
uses: shivammathur/setup-php@v2
@ -179,7 +180,8 @@ jobs:
# Install MailPoet dependencies only if the cache was not hit
- name: Install mailpoet dependencies
if: steps.composer-mailpoet-cache.outputs.cache-hit != 'true'
if: |
steps.composer-mailpoet-cache.outputs.cache-hit != 'true' || steps.vendor-prefixed-cache.outputs.cache-hit != 'true'
run: ./tools/vendor/composer.phar install
working-directory: mailpoet

30
.woodpecker.yml Normal file
View File

@ -0,0 +1,30 @@
clone:
git:
image: woodpeckerci/plugin-git
settings:
depth: 1
steps:
build:
image: node:current-bookworm-slim
commands:
- apt update
- apt install php php-symfony bash -y
- npm install pnpm
- cd mailpoet
- bash build.sh
- mkdir ../output
- mv mailpoet.zip ../output
- cd ..
release:
image: woodpeckerci/plugin-gitea-release:latest
settings:
base_url: https://git.cavemanon.xyz
api_key:
from_secret: releasesmithapikey
files: "output/"
prerelease: false
title: "${CI_COMMIT_TAG}"
when:
- event: tag

View File

@ -46,7 +46,6 @@ services:
NPM_CONFIG_CACHE: '/tmp/.npm'
XDG_CACHE_HOME: '/tmp/.cache'
MAILPOET_DEV_SITE: 1
MP_ENV: development
volumes:
- './wordpress:/var/www/html'
- './tsconfig.base.json:/var/www/html/wp-content/plugins/tsconfig.base.json:ro'
@ -81,7 +80,6 @@ services:
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
PHP_IDE_CONFIG: 'serverName=Mailpoet'
MP_ENV: test
volumes:
- './mailpoet:/var/www/html/wp-content/plugins/mailpoet'
- './mailpoet-premium:/var/www/html/wp-content/plugins/mailpoet-premium'

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@ class RoboFile extends \Robo\Tasks {
return $this->taskExecStack()
->stopOnFail()
->exec('./tools/vendor/composer.phar install')
->exec('cd ../packages/php/email-editor && ../../../mailpoet/tools/vendor/composer.phar install && cd -')
->exec('cd .. && pnpm install --frozen-lockfile --prefer-offline')
->addCode([$this, 'cleanupCachedFiles'])
->run();
@ -32,6 +33,7 @@ class RoboFile extends \Robo\Tasks {
return $this->taskExecStack()
->stopOnFail()
->exec('./tools/vendor/composer.phar install')
->exec('cd ../packages/php/email-editor && ../../../mailpoet/tools/vendor/composer.phar install && cd -')
->addCode([$this, 'cleanupCachedFiles'])
->run();
}

View File

@ -161,3 +161,12 @@ ul.sending-method-benefits {
.mailpoet_install_premium_message {
margin-bottom: $grid-gap-medium;
}
.mailpoet-verify-key-button {
height: 36px;
}
.mailpoet-premium-key-toggle {
height: 34px;
padding: 0 10px !important;
}

View File

@ -53,7 +53,7 @@ function exportMixpanel() {
if (
window.mailpoet_analytics_enabled &&
window.MailPoet.libs3rdPartyEnabled
window.mailpoet_3rd_party_libs_enabled
) {
window.MailPoet.trackEvent = track;
} else {

View File

@ -16,7 +16,11 @@ export type ApiError = {
export const initializeApi = () => {
apiFetch.use((options, next) => {
if (options.path && options.path.startsWith('/wc-analytics/')) {
if (
options.path &&
(options.path.startsWith('/wc-analytics/') ||
options.path.startsWith('/wp/v2/'))
) {
return apiFetch.createRootURLMiddleware(`${api.root}/`)(options, next);
}
return apiFetch.createRootURLMiddleware(apiUrl)(options, next);

View File

@ -143,6 +143,7 @@ export function* activate() {
return {
type: 'ACTIVATE',
automation: data?.data ?? automation,
saved: !!data?.data,
} as const;
}

View File

@ -41,11 +41,15 @@ export function reducer(state: State, action): State {
savedState: 'saved',
};
case 'ACTIVATE':
return {
...state,
automationData: action.automation,
savedState: 'saved',
};
return action.saved
? {
...state,
automationData: action.automation,
savedState: 'saved',
}
: {
...state,
};
case 'DEACTIVATE':
return {
...state,

View File

@ -3,6 +3,7 @@ import { Step } from '../../../../../editor/components/automation/types';
import { storeName } from '../../../../../editor/store';
const transactionalTriggers = [
'mailpoet:custom-trigger',
'woocommerce:order-status-changed',
'woocommerce:order-created',
'woocommerce:order-completed',
@ -15,6 +16,9 @@ const transactionalTriggers = [
'woocommerce-subscriptions:subscription-status-changed',
'woocommerce-subscriptions:trial-ended',
'woocommerce-subscriptions:trial-started',
'woocommerce:buys-from-a-tag',
'woocommerce:buys-from-a-category',
'woocommerce:buys-a-product',
];
export function isTransactional(step: Step): boolean {

View File

@ -7,6 +7,7 @@ import { step as AbandonedCartTrigger } from './steps/abandoned-cart';
import { MailPoet } from '../../../mailpoet';
import { step as BuysAProductTrigger } from './steps/buys-a-product';
import { step as BuysFromACategory } from './steps/buys-from-a-category';
import { step as BuysFromATag } from './steps/buys-from-a-tag';
import { step as MadeAReview } from './steps/made-a-review';
// Insert new imports here
@ -21,6 +22,7 @@ export const initialize = (): void => {
registerStepType(AbandonedCartTrigger);
registerStepType(BuysAProductTrigger);
registerStepType(BuysFromACategory);
registerStepType(BuysFromATag);
registerStepType(MadeAReview);
// Insert new steps here
};

View File

@ -0,0 +1,86 @@
import { useEffect, useMemo, useState } from 'react';
import { Search } from '@woocommerce/components';
import { __ } from '@wordpress/i18n';
import { PanelBody } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
import { dispatch, useSelect } from '@wordpress/data';
import { PlainBodyTitle } from '../../../../../editor/components';
import { storeName } from '../../../../../editor/store';
import { OrderStatusPanel } from '../../order-status-changed/edit/order-status-panel';
import autocompleter from './tag-autocompleter';
type Tag = {
key: string | number;
label?: string;
};
async function fetchTags(include: number[], callback: (tags: Tag[]) => void) {
const path = addQueryArgs('/wp/v2/product_tag/', { include });
const data: { id: number; name: string }[] = await apiFetch({
path,
method: 'GET',
});
callback(data.map((item) => ({ key: item?.id, label: item?.name })));
}
export function Edit(): JSX.Element {
const [current, setCurrent] = useState<Tag[]>([]);
const { selectedStep } = useSelect((select) => ({
selectedStep: select(storeName).getSelectedStep(),
}));
const tagIds: number[] = useMemo(
() => (selectedStep.args?.tag_ids as number[]) ?? [],
[selectedStep],
);
const [isBusy, setIsBusy] = useState(tagIds.length > 0);
useEffect(() => {
if (!isBusy) {
return;
}
void fetchTags(tagIds, (tags: Tag[]) => {
setCurrent(tags);
setIsBusy(false);
});
}, [isBusy, tagIds]);
return (
<>
<PanelBody opened>
<PlainBodyTitle title={__('Tags', 'mailpoet')} />
<Search
disabled={isBusy}
type="custom"
autocompleter={autocompleter}
className={`mailpoet-product-search ${isBusy ? 'is-busy' : ''}`}
placeholder={__('Search for a tag', 'mailpoet')}
selected={current}
onChange={(items: Tag[]) => {
setCurrent(items);
void dispatch(storeName).updateStepArgs(
selectedStep.id,
'tag_ids',
items.map((item) => item.key),
);
}}
multiple
inlineTags
/>
</PanelBody>
<OrderStatusPanel
label={__('Order settings', 'mailpoet')}
showFrom={false}
showTo
toLabel={__('Order status', 'mailpoet')}
onChange={(status, property) => {
void dispatch(storeName).updateStepArgs(
selectedStep.id,
property,
status,
);
}}
/>
</>
);
}

View File

@ -0,0 +1,67 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { AutoCompleter } from '@woocommerce/components/build-types/search/autocompleters';
const tagAutoCompleter: AutoCompleter = {
name: 'tags',
className: 'woocommerce-search__product-result',
options(search) {
const query = search
? {
search,
per_page: 10,
orderby: 'count',
}
: {};
return apiFetch({
path: addQueryArgs('/wp/v2/product_tag', query),
});
},
isDebounced: true,
getOptionIdentifier(tag) {
return tag.id as number;
},
getOptionKeywords(tag) {
return [tag.name] as string[];
},
getFreeTextOptions(query) {
const label = (
<span key="name" className="woocommerce-search__result-name">
{__('Search results', 'mailpoet')}
</span>
);
const titleOption = {
key: 'title',
label,
value: { id: query, name: query },
};
return [titleOption];
},
getOptionLabel(tag) {
return (
<span
key="name"
className="woocommerce-search__result-name"
aria-label={tag.name}
>
{tag.name}
</span>
);
},
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
// of replace/insertion, so we can just return the value.
getOptionCompletion(tag) {
const value = {
key: tag.id,
label: tag.name,
};
return value;
},
};
export default tagAutoCompleter;

View File

@ -0,0 +1,26 @@
export function Icon(): JSX.Element {
return (
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.73173 4.00134C5.8081 3.03517 6.61579 2.27551 7.60166 2.27551C8.58753 2.27551 9.39522 3.03517 9.47159 4.00134H10.1904C10.7479 4.00134 11.2033 4.45681 11.2033 5.01426V10.1917C11.2033 10.7492 10.7479 11.2047 10.1904 11.2047H5.01292C4.45547 11.2047 4 10.7492 4 10.1917V5.01426C4 4.45681 4.45547 4.00134 5.01292 4.00134H5.73173ZM7.60166 3.43843C7.36389 3.43843 7.15161 3.55664 7.02145 3.73861C6.96586 3.81633 6.92553 3.90533 6.90474 4.00134H8.29858C8.27779 3.90533 8.23746 3.81633 8.18187 3.73861C8.05171 3.55664 7.83943 3.43843 7.60166 3.43843ZM5.16291 5.16426V9.65285C5.16291 9.86763 5.33703 10.0417 5.55181 10.0417H9.65151C9.86629 10.0417 10.0404 9.86763 10.0404 9.65285V5.16426H9.47749V5.87717C9.47749 6.19732 9.21618 6.45863 8.89603 6.45863C8.57589 6.45863 8.31458 6.19732 8.31458 5.87717V5.16426H6.88875V5.87717C6.88875 6.19732 6.62743 6.45863 6.30729 6.45863C5.98714 6.45863 5.72583 6.19732 5.72583 5.87717V5.16426H5.16291Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.5858 4.58722C13.9609 4.21215 14.4696 4.00143 15 4.00143H18C18.5304 4.00143 19.0391 4.21215 19.4142 4.58722C19.7893 4.96229 20 5.471 20 6.00143V9.00143C20 9.53187 19.7893 10.0406 19.4142 10.4156C19.0391 10.7907 18.5304 11.0014 18 11.0014H15C14.4696 11.0014 13.9609 10.7907 13.5858 10.4156C13.2107 10.0406 13 9.53187 13 9.00143V6.00143C13 5.471 13.2107 4.96229 13.5858 4.58722ZM15 5.50143H18C18.1326 5.50143 18.2598 5.55411 18.3536 5.64788C18.4473 5.74165 18.5 5.86883 18.5 6.00143V9.00143C18.5 9.13404 18.4473 9.26122 18.3536 9.35499C18.2598 9.44876 18.1326 9.50143 18 9.50143H15C14.8674 9.50143 14.7402 9.44876 14.6464 9.35499C14.5527 9.26122 14.5 9.13404 14.5 9.00143V6.00143C14.5 5.86883 14.5527 5.74165 14.6464 5.64788C14.7402 5.55411 14.8674 5.50143 15 5.50143Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.5858 13.5844C13.9609 13.2093 14.4696 12.9986 15 12.9986H18C18.5304 12.9986 19.0391 13.2093 19.4142 13.5844C19.7893 13.9594 20 14.4681 20 14.9986V17.9986C20 18.529 19.7893 19.0377 19.4142 19.4128C19.0391 19.7879 18.5304 19.9986 18 19.9986H15C14.4696 19.9986 13.9609 19.7879 13.5858 19.4128C13.2107 19.0377 13 18.529 13 17.9986V14.9986C13 14.4681 13.2107 13.9594 13.5858 13.5844ZM15 14.4986H18C18.1326 14.4986 18.2598 14.5513 18.3536 14.645C18.4473 14.7388 18.5 14.866 18.5 14.9986V17.9986C18.5 18.1312 18.4473 18.2584 18.3536 18.3521C18.2598 18.4459 18.1326 18.4986 18 18.4986H15C14.8674 18.4986 14.7402 18.4459 14.6464 18.3521C14.5527 18.2584 14.5 18.1312 14.5 17.9986V14.9986C14.5 14.866 14.5527 14.7388 14.6464 14.645C14.7402 14.5513 14.8674 14.4986 15 14.4986Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.58579 13.5844C4.21071 13.9594 4 14.4681 4 14.9986V17.9986C4 18.529 4.21071 19.0377 4.58579 19.4128C4.96086 19.7879 5.46957 19.9986 6 19.9986H9C9.53043 19.9986 10.0391 19.7879 10.4142 19.4128C10.7893 19.0377 11 18.529 11 17.9986V14.9986C11 14.4681 10.7893 13.9594 10.4142 13.5844C10.0391 13.2093 9.53043 12.9986 9 12.9986H6C5.46957 12.9986 4.96086 13.2093 4.58579 13.5844ZM9 14.4986H6C5.86739 14.4986 5.74021 14.5513 5.64645 14.645C5.55268 14.7388 5.5 14.866 5.5 14.9986V17.9986C5.5 18.1312 5.55268 18.2584 5.64645 18.3521C5.74021 18.4459 5.86739 18.4986 6 18.4986H9C9.13261 18.4986 9.25979 18.4459 9.35355 18.3521C9.44732 18.2584 9.5 18.1312 9.5 17.9986V14.9986C9.5 14.866 9.44732 14.7388 9.35355 14.645C9.25979 14.5513 9.13261 14.4986 9 14.4986Z"
/>
</svg>
);
}

View File

@ -0,0 +1,38 @@
import { __ } from '@wordpress/i18n';
import { StepType } from '../../../../editor/store';
import { Edit } from './edit';
import { Icon } from './icon';
const keywords = [
// translators: noun, used as a search keyword for "Customer buys from a tag" trigger
__('tag', 'mailpoet'),
// translators: verb, used as a search keyword for "Customer buys from a tag" trigger
__('buy', 'mailpoet'),
// translators: verb, used as a search keyword for "Customer buys from a tag" trigger
__('purchase', 'mailpoet'),
// translators: noun, used as a search keyword for "Customer buys from a tag" trigger
__('ecommerce', 'mailpoet'),
// translators: noun, used as a search keyword for "Customer buys from a tag" trigger
__('woocommerce', 'mailpoet'),
// translators: noun, used as a search keyword for "Customer buys from a tag" trigger
__('product', 'mailpoet'),
// translators: noun, used as a search keyword for "Customer buys from a tag" trigger
__('order', 'mailpoet'),
];
export const step: StepType = {
key: 'woocommerce:buys-from-a-tag',
group: 'triggers',
title: () => __('Customer buys from a tag', 'mailpoet'),
description: () =>
__(
'Start the automation when a customer buys a product from a tag.',
'mailpoet',
),
subtitle: () => __('Trigger', 'mailpoet'),
keywords,
foreground: '#2271b1',
background: '#f0f6fc',
icon: () => <Icon />,
edit: () => <Edit />,
} as const;

View File

@ -4,27 +4,27 @@ import { Icon } from './icon';
import { PremiumModalForStepEdit } from '../../../../components/premium-modal-steps-edit';
const keywords = [
// translators: noun, used as a search keyword for "Customer makes a review" trigger
// translators: noun, used as a search keyword for "Customer posts a review" trigger
__('review', 'mailpoet'),
// translators: verb, used as a search keyword for "Customer makes a review" trigger
// translators: verb, used as a search keyword for "Customer posts a review" trigger
__('buy', 'mailpoet'),
// translators: noun, used as a search keyword for "Customer makes a review" trigger
// translators: noun, used as a search keyword for "Customer posts a review" trigger
__('comment', 'mailpoet'),
// translators: noun, used as a search keyword for "Customer makes a review" trigger
// translators: noun, used as a search keyword for "Customer posts a review" trigger
__('ecommerce', 'mailpoet'),
// translators: noun, used as a search keyword for "Customer makes a review" trigger
// translators: noun, used as a search keyword for "Customer posts a review" trigger
__('woocommerce', 'mailpoet'),
// translators: noun, used as a search keyword for "Customer makes a review" trigger
// translators: noun, used as a search keyword for "Customer posts a review" trigger
__('product', 'mailpoet'),
// translators: noun, used as a search keyword for "Customer makes a review" trigger
// translators: noun, used as a search keyword for "Customer posts a review" trigger
__('order', 'mailpoet'),
];
export const step: StepType = {
key: 'woocommerce:made-a-review',
group: 'triggers',
title: () => __('Customer makes a review', 'mailpoet'),
title: () => __('Customer posts a review', 'mailpoet'),
description: () =>
__('Start the automation when a customer makes a review.', 'mailpoet'),
__('Start the automation when a customer posts a review.', 'mailpoet'),
subtitle: () => __('Trigger', 'mailpoet'),
keywords,

View File

@ -1,21 +1,40 @@
import { Input } from 'common/index';
import { _x } from '@wordpress/i18n';
import { Button, Input } from 'common/index';
import { useAction, useSelector } from 'settings/store/hooks';
import { useState } from 'react';
type KeyInputPropType = {
placeholder?: string;
isFullWidth?: boolean;
forceRevealed?: boolean;
};
export function KeyInput({
placeholder,
isFullWidth = false,
forceRevealed = false,
}: 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="text"
type={inputType}
id="mailpoet_premium_key"
name="premium[premium_key]"
placeholder={placeholder}
@ -29,6 +48,7 @@ export function KeyInput({
key: event.target.value.trim() || null,
})
}
iconEnd={toggleButton}
/>
);
}

View File

@ -134,6 +134,7 @@ interface Window {
mailpoet_date_format: string;
mailpoet_listing_per_page: string;
mailpoet_3rd_party_libs_enabled: string;
mailpoet_analytics_enabled: boolean;
mailpoet_datetime_format: string;
mailpoet_api_version: string;
mailpoet_email_regex: RegExp;
@ -294,4 +295,7 @@ interface Window {
dataInconsistencies: {
[key: string]: number;
};
mailpoet_block_email_editor_enabled: boolean;
satismeter: (action: string, data: Record<string, unknown>) => void;
mailpoet_display_nps_email_editor: boolean;
}

View File

@ -1,4 +1,4 @@
import { Tooltip } from 'help-tooltip.jsx';
import { Tooltip } from './help-tooltip.jsx';
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';

View File

@ -73,7 +73,7 @@ export function ProductDiscovery({ onHide }: Props): JSX.Element {
title={MailPoet.I18n.t('brandWooEmails')}
description={MailPoet.I18n.t('brandWooEmailsDesc')}
link="admin.php?page=mailpoet-settings#/woocommerce"
imgSrc={`${MailPoet.cdnUrl}homepage/woo-transactional-email-illustration.png`}
imgSrc={`${MailPoet.cdnUrl}homepage/woo-transactional-email-illustration.20241219.png`}
isDone={tasksStatus.brandWooEmails}
doneMessage={MailPoet.I18n.t('brandWooEmailsDone')}
/>,

View File

@ -13,7 +13,7 @@ export function Resources(): JSX.Element {
link="https://kb.mailpoet.com/article/141-create-an-email-types-of-campaigns?utm_source=plugin&utm_medium=homepage&utm_campaign=resources"
abstract={MailPoet.I18n.t('createAnEmailAbstract')}
title={MailPoet.I18n.t('createAnEmailTitle')}
imgSrc={`${MailPoet.cdnUrl}homepage/resources/add_email.png`}
imgSrc={`${MailPoet.cdnUrl}homepage/resources/add_email.20241219.png`}
/>,
<ResourcePost
key="createAForm"

View File

@ -12,7 +12,7 @@ const Images = {
icon_4: `${MailPoet.cdnUrl}landingpage/feature_icon_4.png`,
},
wooCommerceFeatureImages: {
feature_1: `${MailPoet.cdnUrl}landingpage/woo_feature_automate_your_marketing.png`,
feature_1: `${MailPoet.cdnUrl}landingpage/woo_feature_automate_your_marketing.20241219.png`,
feature_2: `${MailPoet.cdnUrl}landingpage/woo_feature_measure_revenue_per_email.png`,
feature_3: `${MailPoet.cdnUrl}landingpage/woo_feature_let_your_brand_shine.png`,
feature_4: `${MailPoet.cdnUrl}landingpage/woo_feature_rescue_abandoned_carts.png`,

View File

@ -14,7 +14,7 @@
"inserter": false,
"lock": false
},
"apiVersion": 2,
"apiVersion": 3,
"$schema": "https://schemas.wp.org/trunk/block.json",
"attributes": {
"logo": {

View File

@ -2,8 +2,11 @@ import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, RadioControl, Icon } from '@wordpress/components';
import { useState } from 'react';
import metadata from './block.json';
import MailPoetIcon from './mailpoet-icon';
import { PremiumModal } from '../../common/premium-modal';
import './style.scss';
const getCdnUrl = () => window.mailpoet_cdn_url;
const getPremiumPluginStatus = () => window.mailpoet_premium_active;
@ -15,8 +18,31 @@ function LogoImage({
logoSrc: string;
style?: React.CSSProperties;
}): JSX.Element {
const [isModalOpened, setIsModalOpened] = useState(false);
return (
<img src={logoSrc} style={style} alt="Powered by MailPoet" width="100px" />
<>
<button
type="button"
className="mailpoet-email-footer-credit"
onClick={() => setIsModalOpened(true)}
>
<img
src={logoSrc}
style={style}
alt="Powered by MailPoet"
width="100px"
/>
</button>
{!!isModalOpened && (
<PremiumModal onRequestClose={() => setIsModalOpened(false)}>
{__(
'A MailPoet logo will appear in the footer of all emails sent with the free version of MailPoet.',
'mailpoet',
)}
</PremiumModal>
)}
</>
);
}
@ -65,15 +91,6 @@ function Edit({
) as unknown as string,
value: 'light',
},
{
label: (
<LogoImage
logoSrc={`${cdnUrl}email-editor/logo-dark.png`}
style={{ background: '#000000' }}
/>
) as unknown as string,
value: 'dark',
},
]}
onChange={(value) => {
setAttributes({

View File

@ -0,0 +1,28 @@
.mailpoet-email-footer-credit {
appearance: none;
background: none;
border: 0;
cursor: pointer;
margin: 0;
padding: 0;
}
// Those styles are copied from the file _commons.scss but because there is only a few of them
// it's better to keep them here to avoid creating some logic to include another file.
.mailpoet-premium-modal.components-modal__frame {
max-width: 500px;
}
.mailpoet-premium-modal-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 16px;
padding-top: 12px;
}
.mailpoet-premium-modal-error {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}

View File

@ -1,5 +1,6 @@
// Core MailPoet styles. We need this here because the email editor does not register
// under the mailpoet namespace and does not have access to the MailPoet plugin styles
// These styles are required by ReviewRequest (mailpoet/assets/js/src/review-request.tsx) feature
// Settings
// Global variables, config switches. Not producing any CSS.
@ -23,3 +24,6 @@
// Actual UI components.
@import '../../../css/src/components-plugin/legacy-modal';
@import '../../../css/src/components-plugin/review-request.scss';
//Integration
@import './integration';

View File

@ -2,10 +2,54 @@
// We have something similar for the PHP package in `mailpoet/lib/EmailEditor/Integrations`
// Here, we can expose MailPoet specific components for use in the Email editor.
import { addFilter } from '@wordpress/hooks';
import { withNpsPoll } from '../nps-poll';
import { addFilter, addAction } from '@wordpress/hooks';
import { MailPoet } from 'mailpoet';
import { withSatismeterSurvey } from './satismeter-survey';
import './index.scss';
import { useValidationRules } from './validate-email-content';
addFilter('mailpoet_email_editor_wrap_editor_component', 'mailpoet', (editor) =>
withNpsPoll(editor),
withSatismeterSurvey(editor),
);
addFilter(
'mailpoet_email_editor_content_validation_rules',
'mailpoet',
(validationRules: []) => [...validationRules, ...useValidationRules()],
);
const EVENTS_TO_TRACK = [
'email_editor_events_editor_layout_loaded', // email editor was opened
'email_editor_events_template_select_modal_template_selected', // a template was selected from the template-select modal
'email_editor_events_template_select_modal_start_from_scratch_clicked', // start from scratch
'email_editor_events_header_campaign_name_title_updated', // campaign title was used
'email_editor_events_header_preview_dropdown_mobile_selected', // preview option - mobile
'email_editor_events_header_preview_dropdown_desktop_selected', // preview option - desktop
'email_editor_events_header_preview_dropdown_send_test_email_selected', // preview option - send test email
'email_editor_events_sent_preview_email', // preview email sent
'email_editor_events_header_preview_dropdown_preview_in_new_tab_selected', // preview option - in new tab
'email_editor_events_rich_text_with_button_personalization_tags_shortcode_icon_clicked', // personalization_tags modal opened
'email_editor_events_personalization_tags_modal_tag_insert_button_clicked', // personalization_tags inserted
'email_editor_events_rich_text_with_button_input_field_updated', // either subject or preheader updated
'email_editor_events_styles_sidebar_screen_typography_opened', // styles sidebar-typography was seen
'email_editor_events_styles_sidebar_screen_colors_opened', // styles sidebar-colors was seen
'email_editor_events_styles_sidebar_screen_layout_opened', // styles sidebar-layout was seen
'email_editor_events_header_send_button_clicked', // Send button clicked
'email_editor_events_trash_modal_move_to_trash_button_clicked', // Move to trash button was clicked
];
addAction('mailpoet_email_editor_events', 'mailpoet', (editorEvents) => {
const { name, ...data } = editorEvents;
// To prevent going over mixpanel quota, we will limit the number of email editor events we track with mixpanel
// Tracks will log all events. This will be done in MAILPOET-5995
if (EVENTS_TO_TRACK.includes(String(name))) {
MailPoet.trackEvent(name, data);
}
});
// enable email editor event tracking
addFilter(
'mailpoet_email_editor_events_tracking_enabled',
'mailpoet',
() => !!window.mailpoet_analytics_enabled,
);

View File

@ -0,0 +1,10 @@
.mailpoet-editor-feedback-button {
bottom: 5px;
position: absolute;
right: 5px;
}
// To be able to scroll up a bit more in case the sidebar content is long button is displayed over it.
.edit-post-sidebar {
box-sizing: border-box;
padding-bottom: 40px;
}

View File

@ -0,0 +1,63 @@
import { MailPoet } from 'mailpoet';
import { Button } from '@wordpress/components';
import { useLayoutEffect, useState } from '@wordpress/element';
import { commentContent } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { initializeSatismeterSurvey } from 'nps-poll';
const emailEditorSatismeterWriteId = '9qCj2SJBE1s5OhnX5NYfRXu82pEDUB9x';
export function withSatismeterSurvey(Component) {
return function WrappedBySurvey(props) {
const [surveyAvailable, setSurveyAvailable] = useState(false);
const triggerSurvey = () => {
// The survey is configured to open when we track the 'Request feedback' event
window.satismeter('track', { event: 'Request feedback' });
};
useLayoutEffect(() => {
// Initialize Satismeter Survey for the email editor
void initializeSatismeterSurvey(emailEditorSatismeterWriteId)
.then(() => {
if (!window.satismeter) {
return;
}
setSurveyAvailable(true);
// We want to show the survey immediately when there has been enough usage
if (window.mailpoet_display_nps_email_editor) {
window.mailpoet_display_nps_email_editor = false;
void MailPoet.Ajax.post({
api_version: MailPoet.apiVersion,
endpoint: 'user_flags',
action: 'set',
data: {
email_editor_survey_seen: MailPoet.Date.toGmtDatetimeString(
new Date(),
),
},
}).then(triggerSurvey);
}
})
// Survey may fail to initialize when 3rd party libs are not allowed. It is OK we don't need to react.
.catch(() => {});
}, []);
return (
<>
<Component {...props} />
{surveyAvailable && (
<Button
icon={commentContent}
variant="tertiary"
className="mailpoet-editor-feedback-button"
onClick={triggerSurvey}
>
{__('Share feedback', 'mailpoet')}
</Button>
)}
</>
);
};
}

View File

@ -0,0 +1,93 @@
import { useMemo } from '@wordpress/element';
import { createBlock } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { dispatch, useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { store as coreDataStore } from '@wordpress/core-data';
const emailEditorStore = 'email-editor/editor';
const contentLink = `<a data-link-href='[mailpoet/subscription-unsubscribe-url]' contenteditable='false' style='text-decoration: underline;' class='mailpoet-email-editor__personalization-tags-link'>${__(
'Unsubscribe',
'mailpoet',
)}</a> | <a data-link-href='[mailpoet/subscription-manage-url]' contenteditable='false' style='text-decoration: underline;' class='mailpoet-email-editor__personalization-tags-link'>${__(
'Manage subscription',
'mailpoet',
)}</a>`;
export function useValidationRules() {
const { contentBlockId, hasFooter } = useSelect((select) => {
const allBlocks = select(blockEditorStore).getBlocks();
const noBodyBlocks = allBlocks.filter(
(block) =>
block.name !== 'mailpoet/powered-by-mailpoet' &&
block.name !== 'core/post-content',
);
// @ts-expect-error getBlocksByName is not defined in types
const blocks = select(blockEditorStore).getBlocksByName(
'core/post-content',
) as string[] | undefined;
return {
contentBlockId: blocks?.[0],
hasFooter: noBodyBlocks.length > 0,
};
});
/* eslint-disable @typescript-eslint/ban-ts-comment */
const { editedTemplateContent, postTemplateId } = useSelect((mapSelect) => ({
editedTemplateContent:
// @ts-ignore
mapSelect(emailEditorStore).getCurrentTemplateContent() as string,
postTemplateId:
// @ts-ignore
mapSelect(emailEditorStore).getCurrentTemplate()?.id as string,
}));
return useMemo(() => {
const linksParagraphBlock = createBlock('core/paragraph', {
align: 'center',
fontSize: 'small',
content: contentLink,
});
return [
{
id: 'missing-unsubscribe-link',
test: (emailContent: string) =>
!emailContent.includes('[mailpoet/subscription-unsubscribe-url]'),
message: __(
'All emails must include an "Unsubscribe" link.',
'mailpoet',
),
actions: [
{
label: __('Insert link', 'mailpoet'),
onClick: () => {
if (!hasFooter) {
void dispatch(blockEditorStore).insertBlock(
linksParagraphBlock,
undefined,
contentBlockId,
);
} else {
void dispatch(coreDataStore).editEntityRecord(
'postType',
'wp_template',
postTemplateId,
{
content: `
${editedTemplateContent}
<!-- wp:paragraph {"align":"center","fontSize":"small"} -->
${contentLink}
<!-- /wp:paragraph -->
`,
},
);
}
},
},
],
},
];
}, [contentBlockId, postTemplateId, hasFooter, editedTemplateContent]);
}

View File

@ -73,10 +73,16 @@ BehaviorsLookup.TextEditorBehavior = Marionette.Behavior.extend({
return url;
}
return this.documentBaseURI.toAbsolute(
const result = this.documentBaseURI.toAbsolute(
url,
this.options.get('remove_script_host'),
);
// Because TinyMCE contains an issue when inserted URLs ampersands are encoded twice
// We remove one of them to store the URL correctly into the database.
// related GH issues:
// - https://github.com/tinymce/tinymce/issues/9774
// - https://github.com/tinymce/tinymce/issues/9618
return result.replace('&amp;', '&');
},
plugins: this.options.plugins,

View File

@ -34,10 +34,7 @@ const redirectToNewsletterHome = () => {
const getEditorLink = (newsletter: NewsletterType) => {
let editorHref = `?page=mailpoet-newsletter-editor&id=${newsletter.id}`;
if (
MailPoet.FeaturesController.isSupported('gutenberg_email_editor') &&
newsletter.wp_post_id
) {
if (newsletter.wp_post_id) {
editorHref = MailPoet.getBlockEmailEditorUrl(newsletter.wp_post_id);
}
return editorHref;

View File

@ -45,10 +45,22 @@ export function EditorSelectModal({
if (!isModalOpen) {
return null;
}
MailPoet.trackEvent(
'New Email Editor > try new email editor modal opened',
{},
{ send_immediately: true },
);
return (
<Modal
title={__('Try the new email editor', 'mailpoet')}
onRequestClose={onClose}
onRequestClose={() => {
MailPoet.trackEvent(
'New Email Editor > try new email editor modal closed',
);
onClose();
}}
className="mailpoet-new-editor-modal"
>
<div className="mailpoet-new-editor-modal-image">
@ -85,7 +97,12 @@ export function EditorSelectModal({
type="button"
variant="tertiary"
onClick={() => {
onClose();
MailPoet.trackEvent(
'New Email Editor > try new email editor modal cancel button clicked',
{},
{ send_immediately: true },
onClose,
);
}}
>
{__('Cancel', 'mailpoet')}
@ -94,7 +111,14 @@ export function EditorSelectModal({
type="button"
variant="primary"
isBusy={isLoading}
onClick={createNewsletterAndOpenEditor}
onClick={() => {
MailPoet.trackEvent(
'New Email Editor > try new email editor modal create with new editor button clicked',
{},
{ send_immediately: true },
createNewsletterAndOpenEditor,
);
}}
>
{__('Continue', 'mailpoet')}
</Button>

View File

@ -76,12 +76,8 @@ const messages = {
const columns = [
{
name: MailPoet.FeaturesController.isSupported('gutenberg_email_editor')
? 'name'
: 'subject',
label: MailPoet.FeaturesController.isSupported('gutenberg_email_editor')
? __('Name', 'mailpoet')
: __('Subject', 'mailpoet'),
name: 'name',
label: __('Name', 'mailpoet'),
sortable: true,
},
{
@ -114,10 +110,7 @@ const bulkActions = [
const confirmEdit = (newsletter) => {
let editorHref = `?page=mailpoet-newsletter-editor&id=${newsletter.id}`;
if (
MailPoet.FeaturesController.isSupported('gutenberg_email_editor') &&
newsletter.wp_post_id
) {
if (newsletter.wp_post_id) {
editorHref = MailPoet.getBlockEmailEditorUrl(newsletter.wp_post_id);
}

View File

@ -75,10 +75,7 @@ function validateNewsletter(newsletter: NewsLetter) {
// Don't validate emails created in the new editor.
// The editor uses a different data format and will have own validation and also own send panel.
// We are using the send page for the new editor only temporarily.
if (
MailPoet.FeaturesController.isSupported('gutenberg_email_editor') &&
newsletter.wp_post_id !== null
) {
if (newsletter.wp_post_id !== null) {
return undefined;
}
@ -322,6 +319,13 @@ class NewsletterSendComponent extends Component<
thumbnailPromise,
validationError: validateNewsletter(response.data),
});
if (response.data?.wp_post_id) {
MailPoet.trackEvent(
'New Email Editor > Send page opened-Newsletter created by BlockEmailEditor',
);
}
return true;
})
.fail(() => {
@ -471,6 +475,9 @@ class NewsletterSendComponent extends Component<
scheduled: wasScheduled,
'Segment Applied': !!this.state.item.options.filterSegmentId,
segments,
editor: this.state.item.wp_post_id
? 'BlockEmailEditor'
: 'legacyEditor',
});
if (wasScheduled) {
this.context.notices.success(
@ -871,9 +878,7 @@ class NewsletterSendComponent extends Component<
<a
className="mailpoet-link"
href={
MailPoet.FeaturesController.isSupported(
'gutenberg_email_editor',
) && wpPostId
wpPostId
? MailPoet.getBlockEmailEditorUrl(Number(wpPostId))
: `?page=mailpoet-newsletter-editor&id=${Number(
this.props.params.id,

View File

@ -36,9 +36,7 @@ export function NewsletterTypes({
const [isCreating, setIsCreating] = useState(false);
const [isSelectEditorModalOpen, setIsSelectEditorModalOpen] = useState(false);
const isNewEmailEditorEnabled = MailPoet.FeaturesController.isSupported(
'gutenberg_email_editor',
);
const isNewEmailEditorEnabled = window.mailpoet_block_email_editor_enabled;
const setupNewsletter = (type): void => {
if (type !== undefined) {
@ -156,10 +154,19 @@ export function NewsletterTypes({
<Icon icon={chevronDown} size={24} />
</Button>
)}
onToggle={(isOpen) =>
isOpen &&
MailPoet.trackEvent('New Email Editor > create email icon clicked')
}
renderContent={() => (
<MenuItem
variant="tertiary"
onClick={() => setIsSelectEditorModalOpen(true)}
onClick={() => {
setIsSelectEditorModalOpen(true);
MailPoet.trackEvent(
'New Email Editor > creating using new email editor button clicked',
);
}}
>
{__('Create using the new email editor (Alpha)', 'mailpoet')}
</MenuItem>

View File

@ -5,78 +5,101 @@ import satismeter from 'satismeter-loader';
import { ReviewRequest } from 'review-request';
import { getTrackingData } from 'analytics.js';
const useNpsPoll = () => {
useLayoutEffect(() => {
const showReviewRequestModal = () => {
MailPoet.Modal.popup({
width: 800,
template: ReactDOMServer.renderToString(
ReviewRequest({
username:
window.mailpoet_current_wp_user_firstname ||
window.mailpoet_current_wp_user.user_login,
reviewRequestIllustrationUrl:
window.mailpoet_review_request_illustration_url,
installedDaysAgo: window.mailpoet_installed_days_ago,
}),
),
onInit: () => {
document
.getElementById('mailpoet_review_request_not_now')
.addEventListener('click', () => MailPoet.Modal.close());
},
});
};
export const initializeSatismeterSurvey = (writeId = null) => {
const showReviewRequestModal = () => {
MailPoet.Modal.popup({
width: 800,
template: ReactDOMServer.renderToString(
ReviewRequest({
username:
window.mailpoet_current_wp_user_firstname ||
window.mailpoet_current_wp_user.user_login,
reviewRequestIllustrationUrl:
window.mailpoet_review_request_illustration_url,
installedDaysAgo: window.mailpoet_installed_days_ago,
}),
),
onInit: () => {
document
.getElementById('mailpoet_review_request_not_now')
.addEventListener('click', () => MailPoet.Modal.close());
},
});
};
const callSatismeter = (trackingData) => {
const newUsersPollId = '6L479eVPXk7pBn6S';
const oldUsersPollId = 'k0aJAsQAWI2ERyGv';
const formPollId = 'EqOgKsgZd832Sz9w';
const emailEditorPollId = '9qCj2SJBE1s5OhnX5NYfRXu82pEDUB9x';
let writeKey;
if (window.mailpoet_display_nps_email_editor) {
writeKey = emailEditorPollId;
} else if (window.mailpoet_display_nps_form) {
writeKey = formPollId;
} else if (window.mailpoet_is_new_user) {
writeKey = newUsersPollId;
} else {
writeKey = oldUsersPollId;
}
satismeter({
writeKey,
userId: window.mailpoet_current_wp_user.ID + window.mailpoet_site_url,
traits: {
name: window.mailpoet_current_wp_user.user_nicename,
email: window.mailpoet_current_wp_user.user_email,
mailpoetVersion: window.mailpoet_version,
mailpoetPremiumIsActive: window.mailpoet_premium_active,
createdAt: trackingData.installedAtIso,
newslettersSent: trackingData.newslettersSent,
welcomeEmails: trackingData.welcomeEmails,
postnotificationEmails: trackingData.postnotificationEmails,
woocommerceEmails: trackingData.woocommerceEmails,
subscribers: trackingData.subscribers,
lists: trackingData.lists,
sendingMethod: trackingData.sendingMethod,
woocommerceIsInstalled: trackingData.woocommerceIsInstalled,
},
events: {
submit: (response) => {
if (response.rating >= 9 && response.completed) {
showReviewRequestModal();
}
},
},
});
const callSatismeter = (trackingData, customWriteId) => {
const newUsersPollId = '6L479eVPXk7pBn6S';
const oldUsersPollId = 'k0aJAsQAWI2ERyGv';
const formPollId = 'EqOgKsgZd832Sz9w';
let writeKey;
if (customWriteId) {
writeKey = customWriteId;
} else if (window.mailpoet_display_nps_form) {
writeKey = formPollId;
} else if (window.mailpoet_is_new_user) {
writeKey = newUsersPollId;
} else {
writeKey = oldUsersPollId;
}
const traits = {
name: window.mailpoet_current_wp_user.user_nicename,
email: window.mailpoet_current_wp_user.user_email,
mailpoetVersion: window.mailpoet_version,
mailpoetPremiumIsActive: window.mailpoet_premium_active,
createdAt: trackingData.installedAtIso,
newslettersSent: trackingData.newslettersSent,
welcomeEmails: trackingData.welcomeEmails,
postnotificationEmails: trackingData.postnotificationEmails,
woocommerceEmails: trackingData.woocommerceEmails,
subscribers: trackingData.subscribers,
lists: trackingData.lists,
sendingMethod: trackingData.sendingMethod,
woocommerceIsInstalled: trackingData.woocommerceIsInstalled,
woocommerceVersion: trackingData.woocommerceVersion,
WordPressVersion: trackingData.WordPressVersion,
blockTheme: trackingData.blockTheme,
themeVersion: trackingData.themeVersion,
theme: trackingData.theme,
};
if (trackingData.gutenbergVersion) {
traits.gutenbergVersion = trackingData.gutenbergVersion;
}
if (trackingData.wooCommerceVersion) {
traits.wooCommerceVersion = trackingData.wooCommerceVersion;
}
satismeter({
writeKey,
userId: window.mailpoet_current_wp_user.ID + window.mailpoet_site_url,
traits,
events: {
submit: (response) => {
if (response.rating >= 9 && response.completed) {
showReviewRequestModal();
}
},
},
});
};
return new Promise((resolve, reject) => {
if (
window.mailpoet_display_nps_poll &&
window.mailpoet_3rd_party_libs_enabled
) {
getTrackingData().then(({ data }) => callSatismeter(data));
getTrackingData().then(({ data }) => {
callSatismeter(data, writeId);
resolve();
});
} else {
reject();
}
});
};
const useNpsPoll = () => {
useLayoutEffect(() => {
// Survey may fail to initialize when 3rd party libs are not allowed. It is OK. We don't need to react.
initializeSatismeterSurvey().catch(() => {});
}, []);
return null;

View File

@ -12,6 +12,7 @@ import { Reinstall } from './reinstall';
import { RecalculateSubscriberScore } from './recalculate-subscriber-score';
import { Logging } from './logging';
import { BounceAddress } from './bounce-address';
import { CaptchaOnSignup } from './captcha-on-signup';
export function Advanced() {
return (
@ -27,6 +28,7 @@ export function Advanced() {
<ShareData />
<Libs3rdParty />
<Captcha />
<CaptchaOnSignup />
<Reinstall />
<Logging />
<SaveButton />

View File

@ -0,0 +1,44 @@
import { t } from 'common/functions';
import { Radio } from 'common/form/radio/radio';
import { useSelector, useSetting } from 'settings/store/hooks';
import { Inputs, Label } from 'settings/components';
export function CaptchaOnSignup() {
const [enabled, setEnabled] = useSetting(
'captcha',
'on_register_forms',
'enabled',
);
const hasWooCommerce = useSelector('hasWooCommerce')();
return (
<>
<Label
title={t('captchaOnRegisterTitle')}
description={t(
hasWooCommerce
? 'captchaOnRegisterWooActiveDescription'
: 'captchaOnRegisterWooInactiveDescription',
)}
htmlFor=""
/>
<Inputs>
<Radio
id="captcha-on-register-enabled"
value="1"
checked={enabled === '1'}
onCheck={setEnabled}
/>
<label htmlFor="captcha-on-register-enabled">{t('yes')}</label>
<span className="mailpoet-gap" />
<Radio
id="captcha-on-register-disabled"
value=""
checked={enabled === ''}
onCheck={setEnabled}
/>
<label htmlFor="captcha-on-register-disabled">{t('no')}</label>
</Inputs>
</>
);
}

View File

@ -50,6 +50,7 @@ function asObject<T extends Schema>(schema: T) {
function asIs<T>(value: T): T {
return value;
}
export function normalizeSettings(data: Record<string, unknown>): Settings {
const text = asString('');
const disabledCheckbox = asBoolean('1', '0', '0');
@ -135,6 +136,9 @@ export function normalizeSettings(data: Record<string, unknown>): Settings {
recaptcha_secret_token: text,
recaptcha_invisible_site_token: text,
recaptcha_invisible_secret_token: text,
on_register_forms: asObject({
enabled: disabledRadio,
}),
}),
logging: asEnum(['everything', 'errors', 'nothing'], 'errors'),
mta_group: asEnum(['mailpoet', 'website', 'smtp'], 'website'),

View File

@ -65,6 +65,9 @@ export type Settings = {
recaptcha_secret_token: string;
recaptcha_invisible_site_token: string;
recaptcha_invisible_secret_token: string;
on_register_forms: {
enabled: '' | '1';
};
};
logging: 'everything' | 'errors' | 'nothing';
mta_group: 'mailpoet' | 'website' | 'smtp';

View File

@ -4,7 +4,7 @@ function WelcomeWizardStepLayoutBody(props) {
return (
<div className="mailpoet-wizard-step">
<div className="mailpoet-wizard-step-illustration">
<img src={props.illustrationUrl} width="500" alt="" />
<img src={props.illustrationUrl} alt="" />
</div>
<div className="mailpoet-wizard-step-content">{props.children}</div>
</div>

View File

@ -55,6 +55,7 @@ function MSSStepSecondPart(): JSX.Element {
'welcomeWizardMSSSecondPartInputPlaceholder',
)}
isFullWidth
forceRevealed
/>
</label>

View File

@ -43,6 +43,9 @@ if [ -d 'vendor-prefixed' ]; then
mv vendor-prefixed vendor-prefixed-backup
fi
echo '[BUILD] Install email editor dependencies'
cd ../packages/php/email-editor && ../../../mailpoet/tools/vendor/composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-scripts && cd -
# Production libraries.
echo '[BUILD] Fetching production libraries'
mkdir vendor-prefixed

2405
mailpoet/changelog.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -61,6 +61,9 @@
"MailPoet\\Test\\DataGenerator\\": "tests/DataGenerator"
}
},
"replace": {
"soundasleep/html2text": "*"
},
"scripts": {
"pre-install-cmd": [
"@php tools/install.php",

10
mailpoet/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9957467448f215809ac9dc8fbcab2b00",
"content-hash": "51893b0f5ed38d130932b86b48e55b94",
"packages": [
{
"name": "dragonmantank/cron-expression",
@ -73,10 +73,11 @@
"dist": {
"type": "path",
"url": "../packages/php/email-editor",
"reference": "311798cfd57b26bb5df1fc7f97b5732e45603419"
"reference": "53577c5aa3a97e82c58284d48c3aa339cb2a15d4"
},
"require": {
"php": ">=7.4"
"php": ">=7.4",
"soundasleep/html2text": "^2.1"
},
"type": "library",
"autoload": {
@ -101,6 +102,9 @@
],
"code-style-fix": [
"../../../mailpoet/tasks/code_sniffer/vendor/bin/phpcbf -p"
],
"phpstan": [
"php ./tasks/run-phpstan.php"
]
},
"description": "Email editor based on WordPress Gutenberg package.",

View File

@ -298,15 +298,17 @@ class Services extends APIEndpoint {
}
public function pingBridge() {
try {
$bridgePingResponse = $this->bridge->pingBridge();
} catch (\Exception $e) {
$response = $this->bridge->pingBridge();
if ($this->wp->isWpError($response)) {
/** @var \WP_Error $response */
$errorDesc = $this->getErrorDescriptionByCode(Bridge::CHECK_ERROR_UNKNOWN);
return $this->errorResponse([
APIError::UNKNOWN => $e->getMessage(),
APIError::UNKNOWN => "{$errorDesc}: {$response->get_error_message()}",
]);
}
if (!$this->bridge->validateBridgePingResponse($bridgePingResponse)) {
$code = $bridgePingResponse ?: Bridge::CHECK_ERROR_UNKNOWN;
if (!$this->bridge->validateBridgePingResponse($response)) {
$code = $this->wp->wpRemoteRetrieveResponseCode($response) ?: Bridge::CHECK_ERROR_UNKNOWN;
return $this->errorResponse([
APIError::UNKNOWN => $this->getErrorDescriptionByCode($code),
]);

View File

@ -52,9 +52,10 @@ class Help {
* @param array<string, string> $systemInfoData The system info data array.
*/
$systemInfoData = WPFunctions::get()->applyFilters('mailpoet_system_info_data', $this->systemReportCollector->getData(true));
try {
$cronPingUrl = $this->cronHelper->getCronUrl(CronDaemon::ACTION_PING);
$cronPingResponse = $this->cronHelper->pingDaemon();
$cronPingResponse = $this->systemReportCollector->getCronPingResponse();
} catch (\Exception $e) {
$cronPingResponse = __('Cant generate cron URL.', 'mailpoet') . ' (' . $e->getMessage() . ')';
$cronPingUrl = $cronPingResponse;
@ -62,6 +63,7 @@ class Help {
$mailerLog = MailerLog::getMailerLog();
$mailerLog['sent'] = MailerLog::sentSince();
$bridgePingResponse = $this->systemReportCollector->getBridgePingResponse();
$systemStatusData = [
'cron' => [
'url' => $cronPingUrl,
@ -70,16 +72,18 @@ class Help {
],
'mss' => [
'enabled' => $this->bridge->isMailpoetSendingServiceEnabled(),
'isReachable' => $this->bridge->validateBridgePingResponse($this->bridge->pingBridge()),
'isReachable' => $this->bridge->validateBridgePingResponse($bridgePingResponse),
],
'cronStatus' => $this->cronHelper->getDaemon(),
'queueStatus' => $mailerLog,
];
$systemStatusData['cronStatus']['accessible'] = $this->cronHelper->isDaemonAccessible();
$systemStatusData['queueStatus']['tasksStatusCounts'] = $this->scheduledTasksRepository->getCountsPerStatus();
$systemStatusData['queueStatus']['latestTasks'] = array_map(function ($task) {
return $this->buildTaskData($task);
}, $this->scheduledTasksRepository->getLatestTasks(SendingQueue::TASK_TYPE));
$scheduledTasks = $this->scheduledTasksRepository->getLatestTasks(SendingQueue::TASK_TYPE);
$systemStatusData['queueStatus']['latestTasks'] = array_map(fn($task) => $this->buildTaskData($task), $scheduledTasks);
$this->pageRenderer->displayPage(
'help.html',
[
@ -132,6 +136,7 @@ class Help {
$subscriber = $subscribers->first() ? $subscribers->first()->getSubscriber() : null;
}
}
return [
'id' => $task->getId(),
'type' => $task->getType(),

View File

@ -6,6 +6,8 @@ use MailPoet\AdminPages\PageRenderer;
use MailPoet\AutomaticEmails\AutomaticEmails;
use MailPoet\Config\Env;
use MailPoet\Config\Menu;
use MailPoet\EmailEditor\Engine\Dependency_Check;
use MailPoet\EmailEditor\Integrations\MailPoet\DependencyNotice;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Listing\PageLimit;
@ -53,6 +55,10 @@ class Newsletters {
private WooCommerce $wooCommerceSegment;
private Dependency_Check $dependencyCheck;
private DependencyNotice $dependencyNotice;
private CapabilitiesManager $capabilitiesManager;
public function __construct(
@ -70,6 +76,8 @@ class Newsletters {
AuthorizedEmailsController $authorizedEmailsController,
UserFlagsController $userFlagsController,
WooCommerce $wooCommerceSegment,
Dependency_Check $dependencyCheck,
DependencyNotice $dependencyNotice,
CapabilitiesManager $capabilitiesManager
) {
$this->pageRenderer = $pageRenderer;
@ -86,6 +94,8 @@ class Newsletters {
$this->authorizedEmailsController = $authorizedEmailsController;
$this->userFlagsController = $userFlagsController;
$this->wooCommerceSegment = $wooCommerceSegment;
$this->dependencyCheck = $dependencyCheck;
$this->dependencyNotice = $dependencyNotice;
$this->capabilitiesManager = $capabilitiesManager;
}
@ -160,6 +170,8 @@ class Newsletters {
$data['legacy_automatic_emails_notice_dismissed'] = (bool)$this->userFlagsController->get('legacy_automatic_emails_notice_dismissed');
$data['block_email_editor_enabled'] = $this->dependencyCheck->are_dependencies_met(); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$this->dependencyNotice->displayMessageIfNeeded();
$this->pageRenderer->displayPage('newsletters.html', $data);
}

View File

@ -161,11 +161,10 @@ class Reporter {
'WP_MAX_MEMORY_LIMIT' => WP_MAX_MEMORY_LIMIT,
'PHP memory_limit' => ini_get('memory_limit'),
'PHP max_execution_time' => ini_get('max_execution_time'),
'users_can_register' => $this->wp->getOption('users_can_register') ? 'yes' : 'no',
'MailPoet Free version' => MAILPOET_VERSION,
'MailPoet Premium version' => (defined('MAILPOET_PREMIUM_VERSION')) ? MAILPOET_PREMIUM_VERSION : 'N/A',
'Total number of subscribers' => $this->subscribersFeature->getSubscribersCount(),
'Sending Method' => isset($mta['method']) ? $mta['method'] : null,
'Sending Method' => $mta['method'] ?? null,
"Send all site's emails with" => $this->settings->get('send_transactional_emails') ? 'current sending method' : 'default WordPress sending method',
'Date of plugin installation' => $this->settings->get('installed_at'),
'Subscribe in comments' => (boolean)$this->settings->get('subscribe.on_comment.enabled', false),
@ -183,11 +182,17 @@ class Reporter {
'Number of active post notifications' => $newsletters['notifications_count'],
'Number of active welcome emails' => $newsletters['welcome_newsletters_count'],
'Total number of standard newsletters sent' => $newsletters['sent_newsletters_count'],
'Total number of block editor gutenberg newsletters' => $newsletters['total_gutenberg_newsletter_count'],
'Number of block editor gutenberg newsletters sent' => $newsletters['sent_gutenberg_newsletter_count'],
'Number of segments' => isset($segments['dynamic']) ? (int)$segments['dynamic'] : 0,
'Number of lists' => isset($segments['default']) ? (int)$segments['default'] : 0,
'Number of subscriber tags' => $this->tagRepository->countBy([]),
'Site is using block theme' => $this->wp->wpIsBlockTheme(),
'Stop sending to inactive subscribers' => $inactiveSubscribersStatus,
'CAPTCHA setting' => $this->settings->get(CaptchaConstants::TYPE_SETTING_NAME, '') ?: 'disabled',
'Is CAPTCHA on register forms enabled' => $this->settings->get(CaptchaConstants::ON_REGISTER_FORMS_SETTING_NAME, false) ? 'yes' : 'no',
'users_can_register' => $this->wp->getOption('users_can_register') ? 'yes' : 'no',
'Is WooCommerce account creation on "My account" enabled' => $this->wp->getOption('woocommerce_enable_myaccount_registration') ?? 'no',
'Plugin > MailPoet Premium' => $this->wp->isPluginActive('mailpoet-premium/mailpoet-premium.php'),
'Plugin > bounce add-on' => $this->wp->isPluginActive('mailpoet-bounce-handler/mailpoet-bounce-handler.php'),
'Plugin > Bloom' => $this->wp->isPluginActive('bloom-for-publishers/bloom.php'),
@ -412,11 +417,15 @@ class Reporter {
}
public function getTrackingData() {
global $wp_version, $woocommerce; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$newsletters = $this->newslettersRepository->getAnalytics();
$segments = $this->segmentsRepository->getCountsPerType();
$mta = $this->settings->get('mta', []);
$installedAt = new Carbon($this->settings->get('installed_at'));
return [
$theme = $this->wp->wpGetTheme();
$result = [
'WordPressVersion' => $wp_version, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
'pluginGutenberg' => $this->wp->isPluginActive('gutenberg/gutenberg.php'),
'installedAtIso' => $installedAt->format(Carbon::ISO8601),
'newslettersSent' => $newsletters['sent_newsletters_count'],
'welcomeEmails' => $newsletters['welcome_newsletters_count'],
@ -424,9 +433,19 @@ class Reporter {
'woocommerceEmails' => $newsletters['automatic_emails_count'],
'subscribers' => $this->subscribersFeature->getSubscribersCount(),
'lists' => isset($segments['default']) ? (int)$segments['default'] : 0,
'sendingMethod' => isset($mta['method']) ? $mta['method'] : null,
'sendingMethod' => $mta['method'] ?? null,
'woocommerceIsInstalled' => $this->woocommerceHelper->isWooCommerceActive(),
'blockTheme' => $this->wp->wpIsBlockTheme(),
'theme' => $theme->Name, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
'themeVersion' => $theme->Version, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
];
if (defined('GUTENBERG_VERSION')) {
$result['gutenbergVersion'] = GUTENBERG_VERSION;
}
if ($this->woocommerceHelper->isWooCommerceActive()) {
$result['wooCommerceVersion'] = $woocommerce->version;
}
return $result;
}
private function isFilterTypeActive(string $filterType, string $action): bool {

View File

@ -148,7 +148,7 @@ class StepHandler {
}, $subjectEntries));
$step->validate($validationArgs);
$step->run($args, $this->stepRunControllerFactory->createController($args));
$step->run($args, $this->stepRunControllerFactory->createController($args, $logger));
// check if run is not completed by now (e.g., one of if/else branches is empty)
$automationRun = $this->automationRunStorage->getAutomationRun($runId);

View File

@ -2,6 +2,7 @@
namespace MailPoet\Automation\Engine\Control;
use MailPoet\Automation\Engine\Control\StepRunLogger;
use MailPoet\Automation\Engine\Data\StepRunArgs;
class StepRunController {
@ -11,12 +12,17 @@ class StepRunController {
/** @var StepRunArgs */
private $stepRunArgs;
/** @var StepRunLogger */
private $stepRunLogger;
public function __construct(
StepScheduler $stepScheduler,
StepRunArgs $stepRunArgs
StepRunArgs $stepRunArgs,
StepRunLogger $stepRunLogger
) {
$this->stepScheduler = $stepScheduler;
$this->stepRunArgs = $stepRunArgs;
$this->stepRunLogger = $stepRunLogger;
}
public function scheduleProgress(int $timestamp = null): int {
@ -34,4 +40,8 @@ class StepRunController {
public function hasScheduledNextStep(): bool {
return $this->stepScheduler->hasScheduledNextStep($this->stepRunArgs);
}
public function getRunLog(): StepRunLogger {
return $this->stepRunLogger;
}
}

View File

@ -2,6 +2,7 @@
namespace MailPoet\Automation\Engine\Control;
use MailPoet\Automation\Engine\Control\StepRunLogger;
use MailPoet\Automation\Engine\Data\StepRunArgs;
class StepRunControllerFactory {
@ -14,7 +15,7 @@ class StepRunControllerFactory {
$this->stepScheduler = $stepScheduler;
}
public function createController(StepRunArgs $args): StepRunController {
return new StepRunController($this->stepScheduler, $args);
public function createController(StepRunArgs $args, StepRunLogger $logger): StepRunController {
return new StepRunController($this->stepScheduler, $args, $logger);
}
}

View File

@ -100,7 +100,15 @@ class StepRunLogger {
$this->automationRunLogStorage->updateAutomationRunLog($log);
}
private function getLog(): AutomationRunLog {
public function saveLogData(array $data): void {
$log = $this->getLog();
foreach ($data as $key => $value) {
$log->setData($key, $value);
}
$this->automationRunLogStorage->updateAutomationRunLog($log);
}
public function getLog(): AutomationRunLog {
if (!$this->log) {
$this->log = $this->automationRunLogStorage->getAutomationRunLogByRunAndStepId($this->runId, $this->stepId);
}

View File

@ -77,7 +77,7 @@ class AutomationRunLog {
}
}
public function getId(): int {
public function getId(): ?int {
return $this->id;
}

View File

@ -48,7 +48,21 @@ class SendEmailAction implements Action {
25 * DAY_IN_SECONDS, // ~1 month
];
// Retry intervals for sending. These are used when the email address
// is not confirmed, and we need send non-transactional emails.
private const OPTIN_RETRY_INTERVALS = [
1 * MINUTE_IN_SECONDS, // ~1 minute
5 * MINUTE_IN_SECONDS, // ~5 minutes
20 * MINUTE_IN_SECONDS, // ~20 minutes
1 * HOUR_IN_SECONDS, // ~1 hour
12 * HOUR_IN_SECONDS, // ~12 hours
1 * DAY_IN_SECONDS, // ~1 day
];
private const WAIT_OPTIN = 'wait_optin';
private const OPTIN_RETRIES = 'optin_retries';
private const TRANSACTIONAL_TRIGGERS = [
'mailpoet:custom-trigger',
'woocommerce:order-status-changed',
'woocommerce:order-created',
'woocommerce:order-completed',
@ -61,6 +75,9 @@ class SendEmailAction implements Action {
'woocommerce-subscriptions:subscription-status-changed',
'woocommerce-subscriptions:trial-ended',
'woocommerce-subscriptions:trial-started',
'woocommerce:buys-from-a-tag',
'woocommerce:buys-from-a-category',
'woocommerce:buys-a-product',
];
private AutomationController $automationController;
@ -169,42 +186,98 @@ class SendEmailAction implements Action {
public function run(StepRunArgs $args, StepRunController $controller): void {
$newsletter = $this->getEmailForStep($args->getStep());
$subscriber = $this->getSubscriber($args);
$state = null;
if ($args->isFirstRun()) {
// run #1: schedule email sending
$subscriberStatus = $subscriber->getStatus();
if ($newsletter->getType() !== NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL && $subscriberStatus !== SubscriberEntity::STATUS_SUBSCRIBED) {
// translators: %s is the subscriber's status.
throw InvalidStateException::create()->withMessage(sprintf(__("Cannot send the email because the subscriber's status is '%s'.", 'mailpoet'), $subscriberStatus));
}
if ($subscriberStatus === SubscriberEntity::STATUS_BOUNCED) {
// translators: %s is the subscriber's status.
throw InvalidStateException::create()->withMessage(sprintf(__("Cannot send the email because the subscriber's status is '%s'.", 'mailpoet'), $subscriberStatus));
}
$meta = $this->getNewsletterMeta($args);
try {
$this->automationEmailScheduler->createSendingTask($newsletter, $subscriber, $meta);
} catch (Throwable $e) {
throw InvalidStateException::create()->withMessage(__('Could not create sending task.', 'mailpoet'));
if ($this->isOptInRequired($newsletter, $subscriber)) {
$controller->getRunLog()->saveLogData([self::WAIT_OPTIN => 1]);
$this->rerunLater($args->getRunNumber(), $controller, $newsletter, $subscriber);
return;
}
$this->scheduleEmail($args, $newsletter, $subscriber);
} else {
// run #N: check/sync sending status with the automation step
// Re-running for opt-in?
$state = $this->getRunLogData($controller);
if (array_key_exists(self::WAIT_OPTIN, $state) && $state[self::WAIT_OPTIN] === 1) {
if ($this->isOptInRequired($newsletter, $subscriber)) {
$this->rerunLater($args->getRunNumber(), $controller, $newsletter, $subscriber);
return;
}
// Subscriber is now confirmed, so we can schedule an email.
$controller->getRunLog()->saveLogData([
self::WAIT_OPTIN => 0,
self::OPTIN_RETRIES => $args->getRunNumber(),
]);
$this->scheduleEmail($args, $newsletter, $subscriber);
}
// Check/sync sending status with the automation step
$success = $this->checkSendingStatus($args, $newsletter, $subscriber);
if ($success) {
return;
}
}
// Schedule a progress run to sync the email sending status to the automation step.
// Normally, a progress run is executed immediately after sending; we're scheduling
// these runs to poll for the status if sync fails or email never sends (timeout).
$nextInterval = self::POLL_INTERVALS[$args->getRunNumber() - 1] ?? 0;
// At this point, we're re-running to check sending status. We need
// to offset opt-in reruns count from sending reruns.
$runNumber = $args->getRunNumber();
$state = $state ?? $this->getRunLogData($controller);
$optinRetryCount = $state[self::OPTIN_RETRIES] ?? 0;
$runNumber -= $optinRetryCount;
$this->rerunLater($runNumber, $controller, $newsletter, $subscriber);
}
private function scheduleEmail(StepRunArgs $args, NewsletterEntity $newsletter, SubscriberEntity $subscriber): void {
$meta = $this->getNewsletterMeta($args);
try {
$this->automationEmailScheduler->createSendingTask($newsletter, $subscriber, $meta);
} catch (Throwable $e) {
throw InvalidStateException::create()->withMessage(__('Could not create sending task.', 'mailpoet'));
}
}
private function getRunLogData(StepRunController $controller): array {
$runLog = $controller->getRunLog()->getLog();
return $runLog->getData();
}
/**
* Schedule a progress run to sync the email sending status to the automation step.
* Normally, a progress run is executed immediately after sending; we're scheduling
* these runs to poll for the status if sync fails or email never sends (timeout),
* or if we need to wait for subscriber opt-in.
*/
private function rerunLater(int $runNumber, StepRunController $controller, NewsletterEntity $newsletter, SubscriberEntity $subscriber): void {
$nextInterval = self::POLL_INTERVALS[$runNumber - 1] ?? 0;
// Use different intervals when retrying for opt-in.
if ($this->isOptInRequired($newsletter, $subscriber)) {
if ($runNumber > count(self::OPTIN_RETRY_INTERVALS)) {
$subscriberStatus = $subscriber->getStatus();
// translators: %s is the subscriber's status.
throw InvalidStateException::create()->withMessage(sprintf(__("Cannot send the email because the subscriber's status is '%s'.", 'mailpoet'), $subscriberStatus));
}
$nextInterval = self::OPTIN_RETRY_INTERVALS[$runNumber - 1];
}
$controller->scheduleProgress(time() + $nextInterval);
}
private function isOptInRequired(NewsletterEntity $newsletter, SubscriberEntity $subscriber): bool {
$subscriberStatus = $subscriber->getStatus();
if ($newsletter->getType() === NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL) return false;
return $subscriberStatus !== SubscriberEntity::STATUS_SUBSCRIBED;
}
/** @param mixed $data */
public function handleEmailSent($data): void {
if (!is_array($data)) {

View File

@ -302,7 +302,18 @@ class TemplatesFactory {
function (): Automation {
return $this->builder->createFromSequence(
__('Purchased a product', 'mailpoet'),
$this->createPurchasedTemplateBody('woocommerce:order:products')
[
[
'key' => 'woocommerce:buys-a-product',
],
[
'key' => 'mailpoet:send-email',
'args' => [
'name' => __('Important information about your order', 'mailpoet'),
'subject' => __('Important information about your order', 'mailpoet'),
],
],
]
);
},
[
@ -324,7 +335,18 @@ class TemplatesFactory {
function (): Automation {
return $this->builder->createFromSequence(
__('Purchased a product with a tag', 'mailpoet'),
$this->createPurchasedTemplateBody('woocommerce:order:tags')
[
[
'key' => 'woocommerce:buys-from-a-tag',
],
[
'key' => 'mailpoet:send-email',
'args' => [
'name' => __('Important information about your order', 'mailpoet'),
'subject' => __('Important information about your order', 'mailpoet'),
],
],
]
);
},
[
@ -346,7 +368,18 @@ class TemplatesFactory {
function (): Automation {
return $this->builder->createFromSequence(
__('Purchased in a category', 'mailpoet'),
$this->createPurchasedTemplateBody('woocommerce:order:categories')
[
[
'key' => 'woocommerce:buys-from-a-category',
],
[
'key' => 'mailpoet:send-email',
'args' => [
'name' => __('Important information about your order', 'mailpoet'),
'subject' => __('Important information about your order', 'mailpoet'),
],
],
]
);
},
[
@ -355,30 +388,4 @@ class TemplatesFactory {
AutomationTemplate::TYPE_DEFAULT
);
}
private function createPurchasedTemplateBody(string $filterField): array {
return [
[
'key' => 'woocommerce:order-completed',
'filters' => [
'operator' => 'and',
'groups' => [
[
'operator' => 'and',
'filters' => [
['field' => $filterField, 'condition' => 'matches-any-of', 'value' => null],
],
],
],
],
],
[
'key' => 'mailpoet:send-email',
'args' => [
'name' => __('Important information about your order', 'mailpoet'),
'subject' => __('Important information about your order', 'mailpoet'),
],
],
];
}
}

View File

@ -0,0 +1,65 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Integrations\WooCommerce\Triggers;
use MailPoet\Automation\Engine\Data\Field;
use MailPoet\Automation\Engine\Data\Filter;
use MailPoet\Automation\Engine\Data\StepRunArgs;
use MailPoet\Automation\Integrations\Core\Filters\EnumArrayFilter;
use MailPoet\Automation\Integrations\Core\Filters\EnumFilter;
use MailPoet\Validator\Builder;
use MailPoet\Validator\Schema\ObjectSchema;
class BuysFromATagTrigger extends BuysAProductTrigger {
const KEY = 'woocommerce:buys-from-a-tag';
public function getKey(): string {
return self::KEY;
}
public function getName(): string {
// translators: automation trigger title
return __('Customer buys from a tag', 'mailpoet');
}
public function getArgsSchema(): ObjectSchema {
return Builder::object([
'tag_ids' => Builder::array(
Builder::integer()
)->minItems(1)->required(),
'to' => Builder::string()->required()->default('wc-completed'),
]);
}
protected function getFilters(StepRunArgs $args): array {
$triggerArgs = $args->getStep()->getArgs();
$filters = [
Filter::fromArray([
'id' => '',
'field_type' => Field::TYPE_ENUM_ARRAY,
'field_key' => 'woocommerce:order:tags',
'condition' => EnumArrayFilter::CONDITION_MATCHES_ANY_OF,
'args' => [
'value' => $triggerArgs['tag_ids'] ?? [],
],
]),
];
$status = str_replace('wc-', '', $triggerArgs['to'] ?? 'completed');
if ($status === 'any') {
return $filters;
}
$filters[] = Filter::fromArray([
'id' => '',
'field_type' => Field::TYPE_ENUM,
'field_key' => 'woocommerce:order:status',
'condition' => EnumFilter::IS_ANY_OF,
'args' => [
'value' => [$status],
],
]);
return $filters;
}
}

View File

@ -11,6 +11,7 @@ use MailPoet\Automation\Integrations\WooCommerce\SubjectTransformers\WordPressUs
use MailPoet\Automation\Integrations\WooCommerce\Triggers\AbandonedCart\AbandonedCartTrigger;
use MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysAProductTrigger;
use MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysFromACategoryTrigger;
use MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysFromATagTrigger;
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCancelledTrigger;
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCompletedTrigger;
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCreatedTrigger;
@ -35,6 +36,9 @@ class WooCommerceIntegration {
/** @var BuysAProductTrigger */
private $buysAProductTrigger;
/** @var BuysFromATagTrigger */
private $buysFromATagTrigger;
/** @var BuysFromACategoryTrigger */
private $buysFromACategoryTrigger;
@ -67,6 +71,7 @@ class WooCommerceIntegration {
AbandonedCartTrigger $abandonedCartTrigger,
BuysAProductTrigger $buysAProductTrigger,
BuysFromACategoryTrigger $buysFromACategoryTrigger,
BuysFromATagTrigger $buysFromATagTrigger,
AbandonedCartSubject $abandonedCartSubject,
OrderStatusChangeSubject $orderStatusChangeSubject,
OrderSubject $orderSubject,
@ -82,6 +87,7 @@ class WooCommerceIntegration {
$this->abandonedCartTrigger = $abandonedCartTrigger;
$this->buysAProductTrigger = $buysAProductTrigger;
$this->buysFromACategoryTrigger = $buysFromACategoryTrigger;
$this->buysFromATagTrigger = $buysFromATagTrigger;
$this->abandonedCartSubject = $abandonedCartSubject;
$this->orderStatusChangeSubject = $orderStatusChangeSubject;
$this->orderSubject = $orderSubject;
@ -111,6 +117,7 @@ class WooCommerceIntegration {
$registry->addTrigger($this->abandonedCartTrigger);
$registry->addTrigger($this->buysAProductTrigger);
$registry->addTrigger($this->buysFromACategoryTrigger);
$registry->addTrigger($this->buysFromATagTrigger);
$registry->addSubjectTransformer($this->wordPressUserToWooCommerceCustomerTransformer);
}
}

View File

@ -8,6 +8,7 @@ class CaptchaConstants {
const TYPE_RECAPTCHA_INVISIBLE = 'recaptcha-invisible';
const TYPE_DISABLED = null;
const TYPE_SETTING_NAME = 'captcha.type';
const ON_REGISTER_FORMS_SETTING_NAME = 'captcha.on_register_forms.enabled';
public static function isReCaptcha(?string $captchaType) {
return in_array($captchaType, [self::TYPE_RECAPTCHA, self::TYPE_RECAPTCHA_INVISIBLE]);
@ -16,4 +17,8 @@ class CaptchaConstants {
public static function isBuiltIn(?string $captchaType) {
return $captchaType === self::TYPE_BUILTIN;
}
public static function isDisabled(?string $captchaType) {
return $captchaType === self::TYPE_DISABLED || $captchaType === '';
}
}

View File

@ -10,25 +10,26 @@ class CaptchaHooks {
private SettingsController $settings;
private CaptchaValidator $captchaValidator;
private CaptchaRenderer $captchaRenderer;
public function __construct(
SettingsController $settings,
CaptchaValidator $captchaValidator
CaptchaValidator $captchaValidator,
CaptchaRenderer $captchaRenderer
) {
$this->settings = $settings;
$this->captchaValidator = $captchaValidator;
$this->captchaRenderer = $captchaRenderer;
}
public function isEnabled(): bool {
// A transient code to enable incremental development of the feature.
// Later when a setting is introduced, this function will be adjusted.
if (!in_array(getenv('MP_ENV'), ['development', 'test'])) {
if (!$this->settings->get(CaptchaConstants::ON_REGISTER_FORMS_SETTING_NAME, false)) {
return false;
}
return CaptchaConstants::isBuiltIn(
$this->settings->get('captcha.type')
);
$type = $this->settings->get('captcha.type');
return CaptchaConstants::isBuiltIn($type)
|| (CaptchaConstants::isDisabled($type) && $this->captchaRenderer->isSupported());
}
public function renderInWPRegisterForm() {

View File

@ -1,15 +1,13 @@
<?php declare(strict_types = 1);
namespace MailPoet\Config;
namespace MailPoet\Captcha;
use MailPoet\Captcha\CaptchaConstants;
use MailPoet\Captcha\ReCaptchaRenderer;
use MailPoet\Captcha\ReCaptchaValidator;
use MailPoet\Config\Env;
use MailPoet\Config\Renderer as BasicRenderer;
use MailPoet\Settings\SettingsController;
use MailPoet\WP\Functions as WPFunctions;
class HooksReCaptcha {
class ReCaptchaHooks {
const RECAPTCHA_LIB_URL = 'https://www.google.com/recaptcha/api.js';
@ -43,9 +41,7 @@ class HooksReCaptcha {
}
public function isEnabled(): bool {
// A transient code to enable incremental development of the feature.
// Later when a setting is introduced, this function will be adjusted.
if (!in_array(getenv('MP_ENV'), ['development', 'test'])) {
if (!$this->settings->get(CaptchaConstants::ON_REGISTER_FORMS_SETTING_NAME, false)) {
return false;
}

View File

@ -3,6 +3,7 @@
namespace MailPoet\Config;
use MailPoet\Captcha\CaptchaHooks;
use MailPoet\Captcha\ReCaptchaHooks;
use MailPoet\Cron\CronTrigger;
use MailPoet\Form\DisplayFormInWPContent;
use MailPoet\Mailer\WordPress\WordpressMailerReplacer;
@ -72,9 +73,6 @@ class Hooks {
/** @var HooksWooCommerce */
private $hooksWooCommerce;
/** @var HooksReCaptcha */
private $reCaptcha;
/** @var SubscriberChangesNotifier */
private $subscriberChangesNotifier;
@ -87,6 +85,9 @@ class Hooks {
/** @var CaptchaHooks */
private $captchaHooks;
/** @var ReCaptchaHooks */
private $reCaptchaHooks;
/** @var WooSystemInfoController */
private $wooSystemInfoController;
@ -108,7 +109,7 @@ class Hooks {
DisplayFormInWPContent $displayFormInWPContent,
HooksWooCommerce $hooksWooCommerce,
CaptchaHooks $captchaHooks,
HooksReCaptcha $reCaptcha,
ReCaptchaHooks $reCaptchaHooks,
SubscriberHandler $subscriberHandler,
SubscriberChangesNotifier $subscriberChangesNotifier,
WP $wpSegment,
@ -131,7 +132,7 @@ class Hooks {
$this->subscriberHandler = $subscriberHandler;
$this->hooksWooCommerce = $hooksWooCommerce;
$this->captchaHooks = $captchaHooks;
$this->reCaptcha = $reCaptcha;
$this->reCaptchaHooks = $reCaptchaHooks;
$this->subscriberChangesNotifier = $subscriberChangesNotifier;
$this->dotcomLicenseProvisioner = $dotcomLicenseProvisioner;
$this->automateWooHooks = $automateWooHooks;
@ -250,44 +251,6 @@ class Hooks {
);
}
// reCAPTCHA on WP registration form
if ($this->reCaptcha->isEnabled()) {
$this->wp->addAction(
'login_enqueue_scripts',
[$this->reCaptcha, 'enqueueScripts']
);
$this->wp->addAction(
'register_form',
[$this->reCaptcha, 'render']
);
$this->wp->addFilter(
'registration_errors',
[$this->reCaptcha, 'validate'],
10,
3
);
// reCAPTCHA on WC registration form
if ($this->wooHelper->isWooCommerceActive()) {
$this->wp->addAction(
'woocommerce_before_customer_login_form',
[$this->reCaptcha, 'enqueueScripts']
);
$this->wp->addAction(
'woocommerce_register_form',
[$this->reCaptcha, 'render']
);
$this->wp->addAction(
'woocommerce_process_registration_errors',
[$this->reCaptcha, 'validate']
);
}
}
// Manage subscription
$this->wp->addAction(
'admin_post_mailpoet_subscription_update',
@ -635,6 +598,7 @@ class Hooks {
);
}
// CAPTCHA on WP & WC registration forms
public function setupCaptchaOnRegisterForm(): void {
if ($this->captchaHooks->isEnabled()) {
$this->wp->addAction(
@ -662,6 +626,40 @@ class Hooks {
3
);
}
} else if ($this->reCaptchaHooks->isEnabled()) {
$this->wp->addAction(
'login_enqueue_scripts',
[$this->reCaptchaHooks, 'enqueueScripts']
);
$this->wp->addAction(
'register_form',
[$this->reCaptchaHooks, 'render']
);
$this->wp->addFilter(
'registration_errors',
[$this->reCaptchaHooks, 'validate'],
10,
3
);
if ($this->wooHelper->isWooCommerceActive()) {
$this->wp->addAction(
'woocommerce_before_customer_login_form',
[$this->reCaptchaHooks, 'enqueueScripts']
);
$this->wp->addAction(
'woocommerce_register_form',
[$this->reCaptchaHooks, 'render']
);
$this->wp->addAction(
'woocommerce_process_registration_errors',
[$this->reCaptchaHooks, 'validate']
);
}
}
}

View File

@ -15,7 +15,6 @@ use MailPoet\EmailEditor\Engine\Email_Editor;
use MailPoet\EmailEditor\Integrations\Core\Initializer as CoreEmailEditorIntegration;
use MailPoet\EmailEditor\Integrations\MailPoet\Blocks\BlockTypesController;
use MailPoet\EmailEditor\Integrations\MailPoet\EmailEditor as MailpoetEmailEditorIntegration;
use MailPoet\Features\FeaturesController;
use MailPoet\InvalidStateException;
use MailPoet\Migrator\Cli as MigratorCli;
use MailPoet\PostEditorBlocks\PostEditorBlock;
@ -132,9 +131,6 @@ class Initializer {
/** @var BlockTypesController */
private $blockTypesController;
/** @var FeaturesController */
private $featureController;
/** @var Url */
private $urlHelper;
@ -175,7 +171,6 @@ class Initializer {
BlockTypesController $blockTypesController,
MailpoetEmailEditorIntegration $mailpoetEmailEditorIntegration,
CoreEmailEditorIntegration $coreEmailEditorIntegration,
FeaturesController $featureController,
Url $urlHelper
) {
$this->rendererFactory = $rendererFactory;
@ -210,7 +205,6 @@ class Initializer {
$this->mailpoetEmailEditorIntegration = $mailpoetEmailEditorIntegration;
$this->coreEmailEditorIntegration = $coreEmailEditorIntegration;
$this->blockTypesController = $blockTypesController;
$this->featureController = $featureController;
$this->urlHelper = $urlHelper;
}
@ -366,10 +360,8 @@ class Initializer {
$this->subscriberActivityTracker->trackActivity();
$this->postEditorBlock->init();
$this->automationEngine->initialize();
if ($this->featureController->isSupported(FeaturesController::GUTENBERG_EMAIL_EDITOR)) {
$this->blockTypesController->initialize();
$this->emailEditor->initialize();
}
$this->blockTypesController->initialize();
$this->emailEditor->initialize();
$this->wpFunctions->doAction('mailpoet_initialized', MAILPOET_VERSION);
} catch (InvalidStateException $e) {
return $this->handleRunningMigration($e);

View File

@ -45,16 +45,16 @@ class AbandonedCartWorker extends SimpleWorker {
$subscribers = $task->getSubscribers();
if ($subscribers->count() !== 1) {
return false;
return true;
}
$subscriber = isset($subscribers[0]) ? $subscribers[0]->getSubscriber() : null;
if (!$subscriber) {
return false;
return true;
}
$automation = $this->automationStorage->getAutomation((int)$automationId, (int)$automationVersion);
if (!$automation || $automation->getStatus() !== Automation::STATUS_ACTIVE) {
return false;
return true;
}
$this->wp->doAction(

View File

@ -219,6 +219,7 @@ class ContainerConfigurator implements IContainerConfigurator {
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCancelledTrigger::class)->setPublic(true)->setShared(false);
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysAProductTrigger::class)->setPublic(true)->setShared(false);
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysFromACategoryTrigger::class)->setPublic(true)->setShared(false);
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysFromATagTrigger::class)->setPublic(true)->setShared(false);
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject::class)->setPublic(true)->setShared(false);
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderStatusChangeSubject::class)->setPublic(true)->setShared(false);
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Subjects\CustomerSubject::class)->setPublic(true)->setShared(false);
@ -337,6 +338,7 @@ class ContainerConfigurator implements IContainerConfigurator {
$container->autowire(\MailPoet\CustomFields\CustomFieldsRepository::class)->setPublic(true);
// Email Editor
$container->autowire(\MailPoet\EmailEditor\Engine\Email_Editor::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\Dependency_Check::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\Email_Api_Controller::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\Settings_Controller::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\Theme_Controller::class)->setPublic(true);
@ -350,6 +352,7 @@ class ContainerConfigurator implements IContainerConfigurator {
$container->autowire(\MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Preprocessors\Typography_Preprocessor::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\Renderer\Renderer::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\Templates\Templates::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\Templates\Templates_Registry::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\Patterns\Patterns::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Content_Renderer::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Engine\Renderer\ContentRenderer\Blocks_Registry::class)->setPublic(true);
@ -358,8 +361,13 @@ class ContainerConfigurator implements IContainerConfigurator {
$container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\Cli::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\EmailEditor::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\EditorPageRenderer::class)->setPublic(true);
$container->register(\MailPoet\EmailEditor\Engine\Renderer\Css_Inliner::class)
->setPublic(true)
->setFactory([\MailPoet\EmailEditor\Integrations\MailPoet\MailpoetCssInlinerFactory::class, 'create']);
$container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\MailPoetCssInliner::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\EmailApiController::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\EmailEditorPreviewEmail::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\DependencyNotice::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\Blocks\BlockTypesController::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\Blocks\BlockTypes\PoweredByMailpoet::class)->setPublic(true);
$container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\Patterns\PatternsController::class)->setPublic(true);
@ -685,7 +693,7 @@ class ContainerConfigurator implements IContainerConfigurator {
// Tags
$container->autowire(\MailPoet\Tags\TagRepository::class)->setPublic(true);
// CAPTCHA
$container->autowire(\MailPoet\Config\HooksReCaptcha::class)->setPublic(true);
$container->autowire(\MailPoet\Captcha\ReCaptchaHooks::class)->setPublic(true);
$container->autowire(\MailPoet\Captcha\ReCaptchaValidator::class)->setPublic(true);
$container->autowire(\MailPoet\Captcha\ReCaptchaRenderer::class)->setPublic(true);
return $container;

View File

@ -24,8 +24,7 @@ abstract class AbstractBlock {
protected function registerAssets() {
if (null !== $this->getEditorScript()) {
// @todo Would usually just register, but the editor_script are not being loaded in the custom editor.
wp_enqueue_script(
wp_register_script(
$this->getEditorScript('handle'),
$this->getEditorScript('path'),
$this->getEditorScript('dependencies'),
@ -35,8 +34,7 @@ abstract class AbstractBlock {
}
if (null !== $this->getEditorStyle()) {
// @todo Would usually just register, but the editor_script are not being loaded in the custom editor.
wp_enqueue_style(
wp_register_style(
$this->getEditorStyle('handle'),
$this->getEditorStyle('path'),
[],
@ -50,7 +48,7 @@ abstract class AbstractBlock {
if (\WP_Block_Type_Registry::get_instance()->is_registered($this->getBlockType())) {
return;
}
$metadata_path = Env::$assetsPath . '/js/src/mailpoet-custom-email-editor-blocks/' . $this->blockName . '/block.json';
$metadata_path = Env::$assetsPath . '/dist/js/email-editor-blocks/' . $this->blockName . '/block.json';
$block_settings = [
'render_callback' => [$this, 'render'],
'editor_script' => $this->getEditorScript('handle'),
@ -79,8 +77,11 @@ abstract class AbstractBlock {
return $key ? $script[$key] : $script;
}
/**
* Loading styles expect that the file with styles has the name `style`. If we use the name `index` or something else the prefixing of the name is different.
*/
protected function getEditorStyle($key = null) {
$path = Env::$assetsUrl . '/dist/js/email-editor-blocks/style-' . $this->blockName . '-block.css';
$path = Env::$assetsPath . '/dist/js/email-editor-blocks/style-' . $this->blockName . '-block.css';
if (!file_exists($path)) {
return null;

View File

@ -0,0 +1,53 @@
<?php declare(strict_types = 1);
namespace MailPoet\EmailEditor\Integrations\MailPoet;
use MailPoet\Config\AccessControl;
use MailPoet\EmailEditor\Engine\Dependency_Check;
use MailPoet\WP\Functions as WPFunctions;
class DependencyNotice {
private const EMAIL_EDITOR_DEPENDENCY_NOTICE = 'email_editor_dependencies_not_met';
private WPFunctions $wp;
private Dependency_Check $dependencyCheck;
public function __construct(
WPFunctions $wp,
Dependency_Check $dependencyCheck
) {
$this->wp = $wp;
$this->dependencyCheck = $dependencyCheck;
}
public function checkDependenciesAndEventuallyShowNotice(): bool {
if ($this->dependencyCheck->are_dependencies_met()) {
$this->wp->deleteTransient(self::EMAIL_EDITOR_DEPENDENCY_NOTICE);
return false;
}
// For admins, we redirect to newsletters page and show notice there, for other users we display a notice immediately
if ($this->wp->currentUserCan(AccessControl::PERMISSION_MANAGE_EMAILS)) {
$this->wp->setTransient(self::EMAIL_EDITOR_DEPENDENCY_NOTICE, true);
$this->wp->wpSafeRedirect($this->wp->adminUrl('admin.php?page=mailpoet-newsletters'));
return true;
} else {
$this->displayMessage();
return true;
}
}
public function displayMessageIfNeeded(): void {
if ($this->wp->getTransient(self::EMAIL_EDITOR_DEPENDENCY_NOTICE)) {
$this->displayMessage();
}
$this->wp->deleteTransient(self::EMAIL_EDITOR_DEPENDENCY_NOTICE);
}
private function displayMessage(): void {
$dependencyErrorMessage = sprintf(
// translators: %1$s: WordPress version e.g. 6.7
__('This email was created using the new editor, which requires WordPress version %1$s or higher. Please update your setup to continue editing or previewing this email.', 'mailpoet'),
Dependency_Check::MIN_WP_VERSION,
);
echo '<div class="notice notice-warning is-dismissible"><p>' . esc_html($dependencyErrorMessage) . '</p></div>';
}
}

View File

@ -2,7 +2,7 @@
namespace MailPoet\EmailEditor\Integrations\MailPoet;
use MailPoet\API\JSON\API;
use MailPoet\Analytics\Analytics;
use MailPoet\Config\Env;
use MailPoet\Config\Installer;
use MailPoet\Config\ServicesChecker;
@ -10,8 +10,10 @@ use MailPoet\EmailEditor\Engine\Settings_Controller;
use MailPoet\EmailEditor\Engine\Theme_Controller;
use MailPoet\EmailEditor\Engine\User_Theme;
use MailPoet\EmailEditor\Integrations\MailPoet\EmailEditor as EditorInitController;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Settings\SettingsController as MailPoetSettings;
use MailPoet\Settings\UserFlagsController;
use MailPoet\Util\CdnAssetUrl;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\WP\Functions as WPFunctions;
@ -25,6 +27,8 @@ class EditorPageRenderer {
private User_Theme $userTheme;
private DependencyNotice $dependencyNotice;
private CdnAssetUrl $cdnAssetUrl;
private ServicesChecker $servicesChecker;
@ -35,6 +39,10 @@ class EditorPageRenderer {
private NewslettersRepository $newslettersRepository;
private UserFlagsController $userFlagsController;
private Analytics $analytics;
public function __construct(
WPFunctions $wp,
Settings_Controller $settingsController,
@ -43,8 +51,11 @@ class EditorPageRenderer {
SubscribersFeature $subscribersFeature,
Theme_Controller $themeController,
User_Theme $userTheme,
DependencyNotice $dependencyNotice,
MailPoetSettings $mailpoetSettings,
NewslettersRepository $newslettersRepository
NewslettersRepository $newslettersRepository,
UserFlagsController $userFlagsController,
Analytics $analytics
) {
$this->wp = $wp;
$this->settingsController = $settingsController;
@ -53,16 +64,31 @@ class EditorPageRenderer {
$this->subscribersFeature = $subscribersFeature;
$this->themeController = $themeController;
$this->userTheme = $userTheme;
$this->dependencyNotice = $dependencyNotice;
$this->mailpoetSettings = $mailpoetSettings;
$this->newslettersRepository = $newslettersRepository;
$this->userFlagsController = $userFlagsController;
$this->analytics = $analytics;
}
public function render() {
$postId = isset($_GET['post']) ? intval($_GET['post']) : 0;
$post = $this->wp->getPost($postId);
if (!$post instanceof \WP_Post || $post->post_type !== EditorInitController::MAILPOET_EMAIL_POST_TYPE) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$currentPostType = $post->post_type; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
if (!$post instanceof \WP_Post || $currentPostType !== EditorInitController::MAILPOET_EMAIL_POST_TYPE) {
return;
}
$newsletter = $this->newslettersRepository->findOneBy(['wpPost' => $postId]);
if (!$newsletter instanceof NewsletterEntity) {
return;
}
$this->dependencyNotice->checkDependenciesAndEventuallyShowNotice();
// load analytics (mixpanel) library
if ($this->analytics->isEnabled()) {
add_filter('admin_footer', [$this, 'loadAnalyticsModule'], 24);
}
// load mailpoet email editor JS integrations
$editorIntegrationAssetsParams = require Env::$assetsPath . '/dist/js/email_editor_integration/email_editor_integration.asset.php';
@ -109,30 +135,27 @@ class EditorPageRenderer {
$assetsParams['version']
);
$jsonAPIRoot = rtrim($this->wp->escUrlRaw(admin_url('admin-ajax.php')), '/');
$token = $this->wp->wpCreateNonce('mailpoet_token');
$apiVersion = API::CURRENT_VERSION;
$currentUserEmail = $this->wp->wpGetCurrentUser()->user_email;
$this->wp->wpLocalizeScript(
'mailpoet_email_editor',
'MailPoetEmailEditor',
[
'json_api_root' => esc_js($jsonAPIRoot),
'api_token' => esc_js($token),
'api_version' => esc_js($apiVersion),
'cdn_url' => esc_js($this->cdnAssetUrl->generateCdnUrl("")),
'is_premium_plugin_active' => (bool)$this->servicesChecker->isPremiumPluginActive(),
'current_post_type' => esc_js($currentPostType),
'current_post_id' => $post->ID,
'current_wp_user_email' => esc_js($currentUserEmail),
'editor_settings' => $this->settingsController->get_settings(),
'editor_theme' => $this->themeController->get_base_theme()->get_raw_data(),
'user_theme_post_id' => $this->userTheme->get_user_theme_post()->ID,
'urls' => [
'listings' => admin_url('admin.php?page=mailpoet-newsletters'),
'send' => admin_url('admin.php?page=mailpoet-newsletters#/send/' . $newsletter->getId()),
],
]
);
$installedAtDiff = (new \DateTime($this->mailpoetSettings->get('installed_at')))->diff(new \DateTime());
// Survey should be displayed only if there are 2 and more emails and the user hasn't seen it yet
$displaySurvey = ($this->newslettersRepository->getCountOfEmailsWithWPPost() > 1) && !$this->userFlagsController->get(UserFlagsController::EMAIL_EDITOR_SURVEY);
// Renders additional script data that some components require e.g. PremiumModal. This is done here instead of using
// PageRenderer since that introduces other dependencies we want to avoid. Used by getUpgradeInfo.
@ -140,7 +163,7 @@ class EditorPageRenderer {
$installer = new Installer(Installer::PREMIUM_PLUGIN_SLUG);
$inline_script_data = [
'mailpoet_premium_plugin_installed' => Installer::isPluginInstalled(Installer::PREMIUM_PLUGIN_SLUG),
'mailpoet_premium_plugin_active' => $this->servicesChecker->isPremiumPluginActive(),
'mailpoet_premium_active' => $this->servicesChecker->isPremiumPluginActive(),
'mailpoet_premium_plugin_download_url' => $this->subscribersFeature->hasValidPremiumKey() ? $installer->generatePluginDownloadUrl() : null,
'mailpoet_premium_plugin_activation_url' => $installer->generatePluginActivationUrl(Installer::PREMIUM_PLUGIN_PATH),
'mailpoet_has_valid_api_key' => $this->subscribersFeature->hasValidApiKey(),
@ -152,7 +175,7 @@ class EditorPageRenderer {
'mailpoet_subscribers_limit_reached' => $this->subscribersFeature->check(),
// settings needed for Satismeter tracking
'mailpoet_3rd_party_libs_enabled' => $this->mailpoetSettings->get('3rd_party_libs.enabled') === '1',
'mailpoet_display_nps_email_editor' => $this->newslettersRepository->getCountOfEmailsWithWPPost() > 1, // Poll should be displayed only if there are 2 and more emails
'mailpoet_display_nps_email_editor' => $displaySurvey,
'mailpoet_display_nps_poll' => true,
'mailpoet_current_wp_user' => $this->wp->wpGetCurrentUser()->to_array(),
'mailpoet_current_wp_user_firstname' => $this->wp->wpGetCurrentUser()->user_firstname,
@ -162,7 +185,7 @@ class EditorPageRenderer {
'mailpoet_installed_days_ago' => (int)$installedAtDiff->format('%a'),
];
$this->wp->wpAddInlineScript('mailpoet_email_editor', implode('', array_map(function ($key) use ($inline_script_data) {
return sprintf("var %s=%s;", $key, json_encode($inline_script_data[$key]));
return sprintf("var %s=%s;", $key, wp_json_encode($inline_script_data[$key]));
}, array_keys($inline_script_data))), 'before');
// Load CSS from Post Editor
@ -192,9 +215,14 @@ class EditorPageRenderer {
'/wp/v2/settings',
'/wp/v2/types?context=view',
'/wp/v2/taxonomies?context=view',
'/wp/v2/templates/lookup?slug=' . $templateSlug,
];
if ($templateSlug) {
$routes[] = '/wp/v2/templates/lookup?slug=' . $templateSlug;
} else {
$routes[] = '/wp/v2/mailpoet_email?context=edit&per_page=30&status=publish,sent';
}
// Preload the data for the specified routes
$preloadData = array_reduce(
$routes,
@ -211,4 +239,25 @@ class EditorPageRenderer {
)
);
}
public function loadAnalyticsModule() { // phpcs:ignore -- MissingReturnStatement not required
$publicId = $this->analytics->getPublicId();
$isPublicIdNew = $this->analytics->isPublicIdNew();
// this is required here because of `analytics-event.js` and order of script load and use in `mailpoet-email-editor-integration/index.ts`
$libs3rdPartyEnabled = $this->mailpoetSettings->get('3rd_party_libs.enabled') === '1';
// we need to set this values because they are used in the analytics.html file
?>
<script type="text/javascript"> <?php // phpcs:ignore ?>
window.mailpoet_analytics_enabled = true;
window.mailpoet_analytics_public_id = '<?php echo esc_js($publicId); ?>';
window.mailpoet_analytics_new_public_id = <?php echo wp_json_encode($isPublicIdNew); ?>;
window.mailpoet_3rd_party_libs_enabled = <?php echo wp_json_encode($libs3rdPartyEnabled); ?>;
window.mailpoet_version = '<?php echo esc_js(MAILPOET_VERSION); ?>';
window.mailpoet_premium_version = '<?php echo esc_js((defined('MAILPOET_PREMIUM_VERSION')) ? MAILPOET_PREMIUM_VERSION : ''); ?>';
</script>
<?php
include_once Env::$viewsPath . '/analytics.html';
}
}

View File

@ -4,7 +4,6 @@ namespace MailPoet\EmailEditor\Integrations\MailPoet;
use MailPoet\EmailEditor\Integrations\MailPoet\Patterns\PatternsController;
use MailPoet\EmailEditor\Integrations\MailPoet\Templates\TemplatesController;
use MailPoet\Features\FeaturesController;
use MailPoet\WP\Functions as WPFunctions;
class EmailEditor {
@ -12,8 +11,6 @@ class EmailEditor {
private WPFunctions $wp;
private FeaturesController $featuresController;
private EmailApiController $emailApiController;
private EditorPageRenderer $editorPageRenderer;
@ -30,7 +27,6 @@ class EmailEditor {
public function __construct(
WPFunctions $wp,
FeaturesController $featuresController,
EmailApiController $emailApiController,
EditorPageRenderer $editorPageRenderer,
EmailEditorPreviewEmail $emailEditorPreviewEmail,
@ -40,7 +36,6 @@ class EmailEditor {
PersonalizationTagManager $personalizationTagManager
) {
$this->wp = $wp;
$this->featuresController = $featuresController;
$this->emailApiController = $emailApiController;
$this->editorPageRenderer = $editorPageRenderer;
$this->patternsController = $patternsController;
@ -51,9 +46,6 @@ class EmailEditor {
}
public function initialize(): void {
if (!$this->featuresController->isSupported(FeaturesController::GUTENBERG_EMAIL_EDITOR)) {
return;
}
$this->cli->initialize();
$this->wp->addFilter('mailpoet_email_editor_post_types', [$this, 'addEmailPostType']);
$this->wp->addAction('rest_delete_mailpoet_email', [$this->emailApiController, 'trashEmail'], 10, 1);

View File

@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace MailPoet\EmailEditor\Integrations\MailPoet;
use MailPoet\EmailEditor\Engine\Renderer\Css_Inliner;
use MailPoetVendor\Pelago\Emogrifier\CssInliner;
class MailPoetCssInliner implements Css_Inliner {
private CssInliner $inliner;
public function from_html(string $unprocessed_html): self {// phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps -- we need to match the interface
$that = new self();
$that->inliner = CssInliner::fromHtml($unprocessed_html);
return $that;
}
public function inline_css(string $css = ''): self {// phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps -- we need to match the interface
if (!isset($this->inliner)) {
throw new \LogicException('You must call from_html before calling inline_css');
}
$this->inliner->inlineCss($css);
return $this;
}
public function render(): string {
if (!isset($this->inliner)) {
throw new \LogicException('You must call from_html before calling inline_css');
}
return $this->inliner->render();
}
}

View File

@ -0,0 +1,9 @@
<?php declare(strict_types = 1);
namespace MailPoet\EmailEditor\Integrations\MailPoet;
class MailpoetCssInlinerFactory {
public static function create(): MailPoetCssInliner {
return new MailPoetCssInliner();
}
}

View File

@ -6,15 +6,15 @@ use MailPoet\EmailEditor\Integrations\MailPoet\Patterns\Pattern;
class OneColumn extends Pattern {
protected $name = '1-column-content';
protected $block_types = ['core/post-content']; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
protected $block_types = []; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
protected $template_types = ['email-template']; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
protected $categories = ['email-contents'];
protected function get_content(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return '
<!-- wp:group {"style":{"spacing":{"padding":{"right":"var:preset|spacing|20","left":"var:preset|spacing|20"}}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group" style="padding-right:var(--wp--preset--spacing--20);padding-left:var(--wp--preset--spacing--20)"><!-- wp:heading {"fontSize":"large"} -->
<h2 class="wp-block-heading has-large-font-size">' . __('1 column layout', 'mailpoet') . '</h2>
<div class="wp-block-group" style="padding-right:var(--wp--preset--spacing--20);padding-left:var(--wp--preset--spacing--20)"><!-- wp:heading -->
<h2 class="wp-block-heading">' . __('1 column layout', 'mailpoet') . '</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->

View File

@ -6,20 +6,20 @@ use MailPoet\EmailEditor\Integrations\MailPoet\Patterns\Pattern;
class ThreeColumn extends Pattern {
protected $name = '3-column-content';
protected $block_types = ['core/post-content']; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
protected $block_types = []; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
protected $template_types = ['email-template']; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
protected $categories = ['email-contents'];
protected function get_content(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return '<!-- wp:group {"style":{"spacing":{"padding":{"top":"var:preset|spacing|10","bottom":"var:preset|spacing|10","left":"var:preset|spacing|20","right":"var:preset|spacing|20"}}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group" style="padding-top:var(--wp--preset--spacing--10);padding-right:var(--wp--preset--spacing--20);padding-bottom:var(--wp--preset--spacing--10);padding-left:var(--wp--preset--spacing--20)"><!-- wp:heading {"fontSize":"large"} -->
<h2 class="wp-block-heading has-large-font-size">' . __('3 column layout', 'mailpoet') . '</h2>
<div class="wp-block-group" style="padding-top:var(--wp--preset--spacing--10);padding-right:var(--wp--preset--spacing--20);padding-bottom:var(--wp--preset--spacing--10);padding-left:var(--wp--preset--spacing--20)"><!-- wp:heading -->
<h2 class="wp-block-heading">' . __('3 column layout', 'mailpoet') . '</h2>
<!-- /wp:heading --></div>
<!-- /wp:group -->
<!-- wp:columns {"style":{"spacing":{"padding":{"right":"var:preset|spacing|10","left":"var:preset|spacing|10"}}},"metadata":{"categories":["email-contents"],"patternName":"mailpoet/1-column-content"}} -->
<div class="wp-block-columns" style="padding-right:var(--wp--preset--spacing--10);padding-left:var(--wp--preset--spacing--10)"><!-- wp:column {"backgroundColor":"white","style":{"spacing":{"padding":{"top":"0","bottom":"0","left":"var:preset|spacing|10","right":"var:preset|spacing|10"}}}} -->
<div class="wp-block-column has-white-background-color has-background" style="padding-top:0;padding-right:var(--wp--preset--spacing--10);padding-bottom:0;padding-left:var(--wp--preset--spacing--10)"><!-- wp:image {"scale":"cover"} -->
<div class="wp-block-columns" style="padding-right:var(--wp--preset--spacing--10);padding-left:var(--wp--preset--spacing--10)"><!-- wp:column {"style":{"spacing":{"padding":{"top":"0","bottom":"0","left":"var:preset|spacing|10","right":"var:preset|spacing|10"}}}} -->
<div class="wp-block-column" style="padding-top:0;padding-right:var(--wp--preset--spacing--10);padding-bottom:0;padding-left:var(--wp--preset--spacing--10)"><!-- wp:image {"scale":"cover"} -->
<figure class="wp-block-image"><img alt="" style="object-fit:cover"/></figure>
<!-- /wp:image --></div>
<!-- /wp:column -->
@ -53,6 +53,6 @@ class ThreeColumn extends Pattern {
protected function get_title(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
/* translators: Name of a content pattern used as starting content of an email */
return __('3 Column', 'mailpoet');
return __('3 Columns', 'mailpoet');
}
}

View File

@ -6,14 +6,14 @@ use MailPoet\EmailEditor\Integrations\MailPoet\Patterns\Pattern;
class TwoColumn extends Pattern {
protected $name = '2-column-content';
protected $block_types = ['core/post-content']; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
protected $block_types = []; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
protected $template_types = ['email-template']; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
protected $categories = ['email-contents'];
protected function get_content(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return '<!-- wp:group {"style":{"spacing":{"padding":{"top":"0","bottom":"0","left":"var:preset|spacing|20","right":"var:preset|spacing|20"}}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group" style="padding-top:0;padding-right:var(--wp--preset--spacing--20);padding-bottom:0;padding-left:var(--wp--preset--spacing--20)"><!-- wp:heading {"fontSize":"large"} -->
<h2 class="wp-block-heading has-large-font-size">' . __('2 column layout', 'mailpoet') . '</h2>
<div class="wp-block-group" style="padding-top:0;padding-right:var(--wp--preset--spacing--20);padding-bottom:0;padding-left:var(--wp--preset--spacing--20)"><!-- wp:heading -->
<h2 class="wp-block-heading">' . __('2 column layout', 'mailpoet') . '</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
@ -22,8 +22,8 @@ class TwoColumn extends Pattern {
<!-- /wp:group -->
<!-- wp:columns {"style":{"spacing":{"padding":{"top":"0","bottom":"0","left":"var:preset|spacing|10","right":"var:preset|spacing|10"}}}} -->
<div class="wp-block-columns" style="padding-top:0;padding-right:var(--wp--preset--spacing--10);padding-bottom:0;padding-left:var(--wp--preset--spacing--10)"><!-- wp:column {"width":"","backgroundColor":"white","style":{"spacing":{"padding":{"top":"0","bottom":"0","left":"var:preset|spacing|10","right":"var:preset|spacing|10"}}}} -->
<div class="wp-block-column has-white-background-color has-background" style="padding-top:0;padding-right:var(--wp--preset--spacing--10);padding-bottom:0;padding-left:var(--wp--preset--spacing--10)"><!-- wp:image -->
<div class="wp-block-columns" style="padding-top:0;padding-right:var(--wp--preset--spacing--10);padding-bottom:0;padding-left:var(--wp--preset--spacing--10)"><!-- wp:column {"width":"","style":{"spacing":{"padding":{"top":"0","bottom":"0","left":"var:preset|spacing|10","right":"var:preset|spacing|10"}}}} -->
<div class="wp-block-column" style="padding-top:0;padding-right:var(--wp--preset--spacing--10);padding-bottom:0;padding-left:var(--wp--preset--spacing--10)"><!-- wp:image -->
<figure class="wp-block-image"><img alt=""/></figure>
<!-- /wp:image -->
@ -33,8 +33,8 @@ class TwoColumn extends Pattern {
<!-- /wp:button --></div>
<!-- /wp:buttons -->
<!-- wp:heading {"level":3,"fontSize":"large"} -->
<h3 class="wp-block-heading has-large-font-size">' . __('Heading', 'mailpoet') . '</h3>
<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">' . __('Heading', 'mailpoet') . '</h3>
<!-- /wp:heading -->
<!-- wp:paragraph -->
@ -48,8 +48,8 @@ class TwoColumn extends Pattern {
<!-- /wp:buttons --></div>
<!-- /wp:column -->
<!-- wp:column {"backgroundColor":"white","style":{"spacing":{"padding":{"right":"var:preset|spacing|10","left":"var:preset|spacing|10"}}}} -->
<div class="wp-block-column has-white-background-color has-background" style="padding-right:var(--wp--preset--spacing--10);padding-left:var(--wp--preset--spacing--10)"><!-- wp:image -->
<!-- wp:column {"style":{"spacing":{"padding":{"right":"var:preset|spacing|10","left":"var:preset|spacing|10"}}}} -->
<div class="wp-block-column" style="padding-right:var(--wp--preset--spacing--10);padding-left:var(--wp--preset--spacing--10)"><!-- wp:image -->
<figure class="wp-block-image"><img alt=""/></figure>
<!-- /wp:image -->
@ -69,6 +69,6 @@ class TwoColumn extends Pattern {
protected function get_title(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
/* translators: Name of a content pattern used as starting content of an email */
return __('2 Column', 'mailpoet');
return __('2 Columns', 'mailpoet');
}
}

View File

@ -18,14 +18,14 @@ class Subscriber {
$subscriberEmail = $context['recipient_email'] ?? null;
$subscriber = $subscriberEmail ? $this->subscribersRepository->findOneBy(['email' => $subscriberEmail]) : null;
return $subscriber ? $subscriber->getFirstName() : $args['default'] ?? '';
return ($subscriber && $subscriber->getFirstName()) ? $subscriber->getFirstName() : $args['default'] ?? '';
}
public function getLastName(array $context, array $args = []): string {
$subscriberEmail = $context['recipient_email'] ?? null;
$subscriber = $subscriberEmail ? $this->subscribersRepository->findOneBy(['email' => $subscriberEmail]) : null;
return $subscriber ? $subscriber->getLastName() : $args['default'] ?? '';
return ($subscriber && $subscriber->getLastName()) ? $subscriber->getLastName() : $args['default'] ?? '';
}
public function getEmail(array $context, array $args = []): string {

View File

@ -28,7 +28,7 @@ class Newsletter {
public function getContent(): string {
// translators: This is a text used in a footer on an email <!--[mailpoet/site-title]--> will be replaced with the site title.
$footerText = __('You received this email because you are subscribed to the <!--[mailpoet/site-title]-->', 'mailpoet');
return '<!-- wp:group {"backgroundColor":"white","layout":{"type":"constrained"},"lock":{"move":false,"remove":false}} -->
return '<!-- wp:group {"backgroundColor":"white","layout":{"type":"constrained"},"lock":{"move":false,"remove":true}} -->
<div class="wp-block-group has-white-background-color has-background">
<!-- wp:group {"style":{"spacing":{"padding":{"top":"var:preset|spacing|30","bottom":"var:preset|spacing|10","left":"var:preset|spacing|20","right":"var:preset|spacing|20"}}}} -->
<div
@ -51,7 +51,7 @@ class Newsletter {
<!-- /wp:image -->
</div>
<!-- /wp:group -->
<!-- wp:post-content {"lock":{"move":false,"remove":false},"layout":{"type":"default"}} /-->
<!-- wp:post-content {"lock":{"move":false,"remove":true},"layout":{"type":"default"}} /-->
<!-- wp:group {"style":{"spacing":{"padding":{"right":"var:preset|spacing|20","left":"var:preset|spacing|20","top":"var:preset|spacing|10","bottom":"var:preset|spacing|10"}}}} -->
<div
class="wp-block-group"
@ -73,14 +73,12 @@ class Newsletter {
padding-top: var(--wp--preset--spacing--20);
padding-bottom: var(--wp--preset--spacing--20);
"
>
' . $footerText . '
<br /><a href="[link:subscription_unsubscribe_url]">' . __('Unsubscribe', 'mailpoet') . '</a> |
<a href="[link:subscription_manage_url]">' . __('Manage subscription', 'mailpoet') . '</a>
>' . $footerText . '<br /><a data-link-href="[mailpoet/subscription-unsubscribe-url]" contenteditable="false" style="text-decoration: underline;" class="mailpoet-email-editor__personalization-tags-link">' . __('Unsubscribe', 'mailpoet') . '</a> | <a data-link-href="[mailpoet/subscription-manage-url]" contenteditable="false" style="text-decoration: underline;" class="mailpoet-email-editor__personalization-tags-link">' . __('Manage subscription', 'mailpoet') . '</a>
</p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:group -->
<!-- wp:mailpoet/powered-by-mailpoet {"lock":{"move":true,"remove":true}} /-->
</div>
<!-- /wp:group -->';
}

View File

@ -2,6 +2,8 @@
namespace MailPoet\EmailEditor\Integrations\MailPoet\Templates;
use MailPoet\EmailEditor\Engine\Templates\Template;
use MailPoet\EmailEditor\Engine\Templates\Templates_Registry;
use MailPoet\EmailEditor\Integrations\MailPoet\EmailEditor;
use MailPoet\EmailEditor\Integrations\MailPoet\Templates\Library\Newsletter;
use MailPoet\Util\CdnAssetUrl;
@ -21,19 +23,22 @@ class TemplatesController {
}
public function initialize() {
$this->wp->addAction('mailpoet_email_editor_register_templates', [$this, 'registerTemplates'], 10, 0);
$this->wp->addFilter('mailpoet_email_editor_register_templates', [$this, 'registerTemplates'], 10, 1);
}
public function registerTemplates() {
public function registerTemplates(Templates_Registry $templatesRegistry): Templates_Registry {
$newsletter = new Newsletter($this->cdnAssetUrl);
register_block_template(
$this->templatePrefix . '//' . $newsletter->getSlug(),
[
'title' => $newsletter->getTitle(),
'description' => $newsletter->getDescription(),
'content' => $newsletter->getContent(),
'post_types' => [EmailEditor::MAILPOET_EMAIL_POST_TYPE],
]
$template = new Template(
$this->templatePrefix,
$newsletter->getSlug(),
$newsletter->getTitle(),
$newsletter->getDescription(),
$newsletter->getContent(),
[EmailEditor::MAILPOET_EMAIL_POST_TYPE]
);
$templatesRegistry->register($template);
return $templatesRegistry;
}
}

View File

@ -207,12 +207,18 @@ class NewsletterEntity {
/**
* @deprecated This is here only for backward compatibility with custom shortcodes https://kb.mailpoet.com/article/160-create-a-custom-shortcode
* This can be removed after 2021-08-01
* This can be removed after 2026-01-01
*/
public function __get($key) {
$getterName = 'get' . Helpers::underscoreToCamelCase($key, $capitaliseFirstChar = true);
$callable = [$this, $getterName];
if (is_callable($callable)) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error -- Intended for deprecation warnings
trigger_error(
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- if the function is callable, it's safe to output
"Direct access to \$newsletter->{$key} is deprecated and will be removed after 2026-01-01. Use \$newsletter->{$getterName}() instead.",
E_USER_DEPRECATED
);
return call_user_func($callable);
}
}

View File

@ -86,12 +86,18 @@ class SendingQueueEntity {
/**
* @deprecated This is here only for backward compatibility with custom shortcodes https://kb.mailpoet.com/article/160-create-a-custom-shortcode
* This can be removed after 2021-08-01
* This can be removed after 2026-01-01
*/
public function __get($key) {
$getterName = 'get' . Helpers::underscoreToCamelCase($key, $capitaliseFirstChar = true);
$callable = [$this, $getterName];
if (is_callable($callable)) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error -- Intended for deprecation warnings
trigger_error(
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- if the function is callable, it's safe to output
"Direct access to \$sendingQueue->{$key} is deprecated and will be removed after 2026-01-01. Use \$sendingQueue->{$getterName}() instead.",
E_USER_DEPRECATED
);
return call_user_func($callable);
}
}

View File

@ -232,12 +232,18 @@ class SubscriberEntity {
/**
* @deprecated This is here only for backward compatibility with custom shortcodes https://kb.mailpoet.com/article/160-create-a-custom-shortcode
* This can be removed after 2021-08-01
* This can be removed after 2026-01-01
*/
public function __get($key) {
$getterName = 'get' . Helpers::underscoreToCamelCase($key, $capitaliseFirstChar = true);
$callable = [$this, $getterName];
if (is_callable($callable)) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error -- Intended for deprecation warnings
trigger_error(
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- if the function is callable, it's safe to output
"Direct access to \$subscriber->{$key} is deprecated and will be removed after 2026-01-01. Use \$subscriber->{$getterName}() instead.",
E_USER_DEPRECATED
);
return call_user_func($callable);
}
}

View File

@ -6,13 +6,11 @@ use MailPoetVendor\Doctrine\DBAL\Exception\TableNotFoundException;
class FeaturesController {
const FEATURE_BRAND_TEMPLATES = 'brand_templates';
const GUTENBERG_EMAIL_EDITOR = 'gutenberg_email_editor';
// Define feature defaults in the array below in the following form:
// self::FEATURE_NAME_OF_FEATURE => true,
private $defaults = [
self::FEATURE_BRAND_TEMPLATES => false,
self::GUTENBERG_EMAIL_EDITOR => false,
];
/** @var array|null */

View File

@ -0,0 +1,43 @@
<?php declare(strict_types = 1);
namespace MailPoet\Migrations\App;
use MailPoet\Entities\NewsletterLinkEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Migrator\AppMigration;
/**
* The plugin from the version 5.5.2 to 5.6.1 contained a bug when we stored links containing &amp; and in some cases also links with `&amp;amp;` in the database.
* This migration fixes the issue by replacing `&amp;amp;` with `&amp; and then &amp; with &`.
*
* See https://mailpoet.atlassian.net/browse/MAILPOET-6433
*/
class Migration_20250120_094614_App extends AppMigration {
public function run(): void {
$sendingQueueId = $this->getSendingQueueId();
if ($sendingQueueId) {
$linksTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName();
$this->entityManager->getConnection()->executeQuery("
UPDATE {$linksTable}
SET url = REPLACE( REPLACE(url, '&amp;amp;', '&amp;'), '&amp;', '&')
WHERE queue_id >= :queue_id;
", ['queue_id' => $sendingQueueId]);
}
}
private function getSendingQueueId(): ?int {
$qb = $this->entityManager->createQueryBuilder();
/** @var array{id: number}|null $result */
$result = $qb->select('sq.id AS id')
->from(SendingQueueEntity::class, 'sq')
->where(
$qb->expr()->gt('sq.createdAt', ':date')
)
->orderBy('sq.id', 'ASC')
->setMaxResults(1)
->setParameter('date', '2024-12-24:00:00:00')
->getQuery()
->getOneOrNullResult();
return $result ? (int)$result['id'] : null;
}
}

View File

@ -39,7 +39,13 @@ class PostContentManager {
if ($this->wp->hasExcerpt($post)) {
return self::stripShortCodes($this->wp->getTheExcerpt($post));
}
return $this->generateExcerpt($post->post_content); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
return self::stripShortCodes(
$this->wp->applyFilters(
'get_the_excerpt',
$this->generateExcerpt($post->post_content), // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$post
)
);
}
return self::stripShortCodes($post->post_content); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
}

View File

@ -3,7 +3,6 @@
namespace MailPoet\Newsletter;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Features\FeaturesController;
use MailPoet\Services\Bridge;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Validator\ValidationException;
@ -16,23 +15,17 @@ class NewsletterValidator {
/** @var TrackingConfig */
private $trackingConfig;
/** @var FeaturesController */
private $featuresController;
public function __construct(
Bridge $bridge,
TrackingConfig $trackingConfig,
FeaturesController $featuresController
TrackingConfig $trackingConfig
) {
$this->bridge = $bridge;
$this->trackingConfig = $trackingConfig;
$this->featuresController = $featuresController;
}
public function validate(NewsletterEntity $newsletterEntity): ?string {
if (
$this->featuresController->isSupported(FeaturesController::GUTENBERG_EMAIL_EDITOR)
&& $newsletterEntity->getWpPostId() !== null
$newsletterEntity->getWpPostId() !== null
) {
// Temporarily skip validation for emails created via Gutenberg editor
return null;

View File

@ -156,6 +156,28 @@ class NewslettersRepository extends Repository {
->getSingleScalarResult() ?: 0;
}
public function getGutenbergNewsletterSentCount(): int {
return intval($this->entityManager->createQueryBuilder()
->select('COUNT(n.id)')
->from(NewsletterEntity::class, 'n')
->where('n.deletedAt IS NULL')
->andWhere('n.wpPost IS NOT NULL')
->andWhere('n.status IN (:statuses)')
->setParameter('statuses', [NewsletterEntity::STATUS_SENT])
->getQuery()
->getSingleScalarResult());
}
public function getTotalGutenbergNewsletterCount() {
return intval($this->entityManager->createQueryBuilder()
->select('COUNT(n.id)')
->from(NewsletterEntity::class, 'n')
->where('n.deletedAt IS NULL')
->andWhere('n.wpPost IS NOT NULL')
->getQuery()
->getSingleScalarResult());
}
public function getAnalytics(): array {
// for automatic emails join 'event' newsletter option to further group the counts
$eventOptionId = (int)$this->entityManager->createQueryBuilder()
@ -203,6 +225,8 @@ class NewslettersRepository extends Repository {
'product_purchased_emails_count' => $analyticsMap[NewsletterEntity::TYPE_AUTOMATIC][PurchasedProduct::SLUG] ?? 0,
'product_purchased_in_category_emails_count' => $analyticsMap[NewsletterEntity::TYPE_AUTOMATIC][PurchasedInCategory::SLUG] ?? 0,
'abandoned_cart_emails_count' => $analyticsMap[NewsletterEntity::TYPE_AUTOMATIC][AbandonedCart::SLUG] ?? 0,
'total_gutenberg_newsletter_count' => $this->getTotalGutenbergNewsletterCount() ?: 0,
'sent_gutenberg_newsletter_count' => $this->getGutenbergNewsletterSentCount() ?: 0,
];
// Count all campaigns
$analyticsMap[NewsletterEntity::TYPE_AUTOMATIC] = array_sum($analyticsMap[NewsletterEntity::TYPE_AUTOMATIC] ?? []);

View File

@ -36,8 +36,12 @@ class OpenTracking {
if (!empty($template)) {
$template = is_array($template) ? $template : [$template];
array_map(
fn($item) => $item->html($item->toString(true, true, 1) . $openTrackingImage),
$template,
function ($item) use ($openTrackingImage) {
$itemHtml = $item->toString(true, true, 1);
$item->html($itemHtml . $openTrackingImage);
return $item;
},
$template
);
}
}

View File

@ -6,7 +6,6 @@ use MailPoet\Config\Env;
use MailPoet\EmailEditor\Engine\Renderer\Renderer as GuntenbergRenderer;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Features\FeaturesController;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
@ -45,9 +44,6 @@ class Renderer {
/*** @var SendingQueuesRepository */
private $sendingQueuesRepository;
/** @var FeaturesController */
private $featuresController;
private CapabilitiesManager $capabilitiesManager;
public function __construct(
@ -59,7 +55,6 @@ class Renderer {
LoggerFactory $loggerFactory,
NewslettersRepository $newslettersRepository,
SendingQueuesRepository $sendingQueuesRepository,
FeaturesController $featuresController,
CapabilitiesManager $capabilitiesManager
) {
$this->bodyRenderer = $bodyRenderer;
@ -70,7 +65,6 @@ class Renderer {
$this->loggerFactory = $loggerFactory;
$this->newslettersRepository = $newslettersRepository;
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->featuresController = $featuresController;
$this->capabilitiesManager = $capabilitiesManager;
}
@ -88,7 +82,7 @@ class Renderer {
$subject = $subject ?: $newsletter->getSubject();
$wpPostEntity = $newsletter->getWpPost();
$wpPost = $wpPostEntity ? $wpPostEntity->getWpPostInstance() : null;
if ($this->featuresController->isSupported(FeaturesController::GUTENBERG_EMAIL_EDITOR) && $wpPost instanceof \WP_Post) {
if ($wpPost instanceof \WP_Post) {
$renderedNewsletter = $this->guntenbergRenderer->render($wpPost, $subject, $newsletter->getPreheader(), $language, $metaRobots);
} else {
$body = (is_array($newsletter->getBody()))
@ -222,6 +216,15 @@ class Renderer {
foreach ($templateDom->query('img') as $image) {
$image->src = str_replace(' ', '%20', $image->src);
}
foreach ($templateDom->query('a') as $anchor) {
// Fix for a TinyMCE bug in smart paste which encodes & as &amp; which is then additionally encoded to &amp;amp;
// when saving the text block content in the editor
$href = str_replace('&amp;amp;', '&amp;', $anchor->href);
// Replace &amp; with & in the href attributes of anchors. URLs are encoded when TinyMCE extracts Text block content via content.innerHTML.
// Links containing &amp; work when placed in an anchor tag in a browser, but they don't work when we redirect to them for example in tracking.
$href = str_replace('&amp;', '&', $href);
$anchor->href = $href;
}
$template = $templateDom->__toString();
$template = $this->wp->applyFilters(
self::FILTER_POST_PROCESS,

View File

@ -70,11 +70,7 @@ class AutomationEmailScheduler {
->andWhere('st.createdAt >= :runCreatedAt')
->setParameter('newsletter', $email)
->setParameter('subscriber', $subscriber)
// Automation Run is fetched via WPDB and it ignores the gmt_offset. This query is processed via Doctrine.
// Doctrine uses PDO connection and uses offset. So the run's created_at is expected to be provided with the offset.
// By removing one day we make sure the offset is not a problem. It makes no harm as this is only performance optimization.
// After we switch to WPDB we could remove this modification and use the exact created_at.
->setParameter('runCreatedAt', $run->getCreatedAt()->modify('-1 day'))
->setParameter('runCreatedAt', $run->getCreatedAt())
->getQuery()
->getResult();
$result = null;

View File

@ -2,6 +2,7 @@
namespace MailPoet\Newsletter\ViewInBrowser;
use MailPoet\EmailEditor\Integrations\MailPoet\DependencyNotice;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
@ -28,12 +29,16 @@ class ViewInBrowserController {
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var DependencyNotice */
private $dependencyNotice;
public function __construct(
LinkTokens $linkTokens,
NewsletterUrl $newsletterUrl,
NewslettersRepository $newslettersRepository,
ViewInBrowserRenderer $viewInBrowserRenderer,
SendingQueuesRepository $sendingQueuesRepository,
DependencyNotice $dependencyNotice,
SubscribersRepository $subscribersRepository
) {
$this->linkTokens = $linkTokens;
@ -41,6 +46,7 @@ class ViewInBrowserController {
$this->subscribersRepository = $subscribersRepository;
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->newsletterUrl = $newsletterUrl;
$this->dependencyNotice = $dependencyNotice;
$this->newslettersRepository = $newslettersRepository;
}
@ -49,6 +55,9 @@ class ViewInBrowserController {
$isPreview = !empty($data['preview']);
$newsletter = $this->getNewsletter($data);
$subscriber = $this->getSubscriber($data);
if ($newsletter->getWpPostId() && $this->dependencyNotice->checkDependenciesAndEventuallyShowNotice()) {
return '';
}
// if queue and subscriber exist, subscriber must have received the newsletter
$queue = isset($data['queue_id']) ? $this->sendingQueuesRepository->findOneById($data['queue_id']) : null;

View File

@ -34,9 +34,11 @@ class ViewInBrowser {
}
private function displayNewsletter($result) {
header('Content-Type: text/html; charset=utf-8');
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $result;
if ($result) {
header('Content-Type: text/html; charset=utf-8');
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $result;
}
exit;
}

View File

@ -61,8 +61,8 @@ class Bridge {
}
/**
* @deprecated Use non-static function isMailpoetSendingServiceEnabled instead
* @return bool
* @deprecated Use non-static function isMailpoetSendingServiceEnabled instead
*/
public static function isMPSendingServiceEnabled() {
try {
@ -99,18 +99,24 @@ class Bridge {
return !empty($key);
}
/**
* @return array|\WP_Error
*/
public function pingBridge() {
$params = [
'blocking' => true,
'timeout' => 10,
];
$wp = new WPFunctions();
$result = $wp->wpRemoteGet(self::BRIDGE_URL, $params);
return $wp->wpRemoteRetrieveResponseCode($result);
return $wp->wpRemoteGet(self::BRIDGE_URL, $params);
}
public function validateBridgePingResponse($responseCode) {
return $responseCode === 200;
/**
* @return bool
*/
public function validateBridgePingResponse($response) {
$wp = new WPFunctions();
return $wp->wpRemoteRetrieveResponseCode($response) === 200;
}
/**

View File

@ -6,6 +6,7 @@ use MailPoet\Entities\UserFlagEntity;
use MailPoet\WP\Functions as WPFunctions;
class UserFlagsController {
const EMAIL_EDITOR_SURVEY = 'email_editor_survey_seen';
/** @var array|null */
private $data = null;
@ -26,6 +27,7 @@ class UserFlagsController {
'transactional_emails_opt_in_notice_dismissed' => false,
'legacy_automations_notice_dismissed' => false,
'legacy_automatic_emails_notice_dismissed' => false,
self::EMAIL_EDITOR_SURVEY => null,
];
$this->userFlagsRepository = $userFlagsRepository;
}

View File

@ -3,10 +3,11 @@
namespace MailPoet\SystemReport;
use MailPoet\Cron\CronHelper;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Mailer\MailerLog;
use MailPoet\Router\Endpoints\CronDaemon;
use MailPoet\Services\Bridge;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\DataInconsistency\DataInconsistencyController;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
use MailPoet\WP\Functions as WPFunctions;
@ -24,16 +25,37 @@ class SystemReportCollector {
/** @var WooCommerceHelper */
private $wooCommerceHelper;
/** @var DataInconsistencyController */
private $dataInconsistencyController;
/** @var CronHelper */
private $cronHelper;
/** @var string|null */
private $cachedCronPingResponse = null;
/** @var array|\WP_Error|null */
private $cachedBridgePingResponse = null;
/** @var Bridge */
private $bridge;
public function __construct(
SettingsController $settings,
WPFunctions $wp,
SubscribersFeature $subscribersFeature,
WooCommerceHelper $wooCommerceHelper
WooCommerceHelper $wooCommerceHelper,
DataInconsistencyController $dataInconsistencyController,
Bridge $bridge,
CronHelper $cronHelper
) {
$this->settings = $settings;
$this->wp = $wp;
$this->subscribersFeature = $subscribersFeature;
$this->wooCommerceHelper = $wooCommerceHelper;
$this->dataInconsistencyController = $dataInconsistencyController;
$this->bridge = $bridge;
$this->cronHelper = $cronHelper;
}
public function getData($maskApiKey = false) {
@ -62,15 +84,29 @@ class SystemReportCollector {
$premiumKey = $this->maskApiKey($premiumKey);
}
$cronHelper = ContainerWrapper::getInstance()->get(CronHelper::class);
$cronDaemonStatus = $this->cronHelper->getDaemon() ?? [];
try {
$cronPingUrl = $cronHelper->getCronUrl(
CronDaemon::ACTION_PING
);
$cronPingUrl = $this->cronHelper->getCronUrl(CronDaemon::ACTION_PING);
$cronPingResponse = $this->getCronPingResponse();
} catch (\Exception $e) {
$cronPingUrl = __('Cant generate cron URL.', 'mailpoet') . ' (' . $e->getMessage() . ')';
$cronPingResponse = $cronPingUrl;
}
$mailerLog = MailerLog::getMailerLog();
$mailerLog['sent'] = MailerLog::sentSince();
$inconsistencyStatus = $this->dataInconsistencyController->getInconsistentDataStatus();
unset($inconsistencyStatus['total']);
$pingBridgeResponse = $this->getBridgePingResponse();
$pingResponse = $this->wp->isWpError($pingBridgeResponse)
? $pingBridgeResponse->get_error_message() // @phpstan-ignore-line
: $this->wp->wpRemoteRetrieveResponseCode($pingBridgeResponse) . ' HTTP status code';
$ApiKeyState = $this->settings->get(Bridge::API_KEY_STATE_SETTING_NAME . '.state');
$premiumKeyState = $this->settings->get(Bridge::PREMIUM_KEY_STATE_SETTING_NAME . '.state');
// the HelpScout Beacon API has a limit of 20 attribute-value pairs (https://developer.helpscout.com/beacon-2/web/javascript-api/#beacon-session-data)
return [
'PHP version' => PHP_VERSION,
@ -81,29 +117,112 @@ class SystemReportCollector {
'Database version' => $dbVersion,
'Web server' => (!empty($_SERVER["SERVER_SOFTWARE"])) ? sanitize_text_field(wp_unslash($_SERVER["SERVER_SOFTWARE"])) : 'N/A',
'Server OS' => (function_exists('php_uname')) ? php_uname() : 'N/A',
'WP info' => 'WP_MEMORY_LIMIT: ' . WP_MEMORY_LIMIT . ' - WP_MAX_MEMORY_LIMIT: ' . WP_MAX_MEMORY_LIMIT . ' - WP_DEBUG: ' . WP_DEBUG .
' - WordPress language: ' . $this->wp->getLocale() . ' - WordPress timezone: ' . $this->wp->wpTimezoneString(),
'PHP info' => 'PHP max_execution_time: ' . ini_get('max_execution_time') . ' - PHP memory_limit: ' . ini_get('memory_limit') .
' - PHP upload_max_filesize: ' . ini_get('upload_max_filesize') . ' - PHP post_max_size: ' . ini_get('post_max_size'),
'WP info' => $this->formatCompositeField([
'WP_MEMORY_LIMIT' => WP_MEMORY_LIMIT,
'WP_MAX_MEMORY_LIMIT' => WP_MAX_MEMORY_LIMIT,
'WP_DEBUG' => WP_DEBUG,
'WordPress language' => $this->wp->getLocale(),
'WordPress timezone' => $this->wp->wpTimezoneString(),
]),
'PHP info' => $this->formatCompositeField([
'PHP max_execution_time' => ini_get('max_execution_time'),
'PHP memory_limit' => ini_get('memory_limit'),
'PHP upload_max_filesize' => ini_get('upload_max_filesize'),
'PHP post_max_size' => ini_get('post_max_size'),
]),
'Multisite environment?' => (is_multisite() ? 'Yes' : 'No'),
'Current Theme' => $currentTheme->get('Name') .
' (version ' . $currentTheme->get('Version') . ')',
'Active Plugin names' => join(", ", $this->wp->getOption('active_plugins')),
'Sending Method' => $mta['method'],
'MailPoet Sending Service' => $this->formatCompositeField([
'Is reachable' => $this->bridge->validateBridgePingResponse($pingBridgeResponse) ? 'Yes' : 'No',
'Ping response' => $pingResponse,
'API key state' => $ApiKeyState ?? 'Unset',
'Premium key state' => $premiumKeyState ?? 'Unset',
]),
'Sending Frequency' => sprintf(
'%d emails every %d minutes',
$mta['frequency']['emails'],
$mta['frequency']['interval']
),
'MailPoet sending info' => "Send all site's emails with: " . ($this->settings->get('send_transactional_emails') ? 'current sending method' : 'default WordPress sending method') .
' - Task Scheduler method: ' . $this->settings->get('cron_trigger.method') . ' - Cron ping URL: ' . $cronPingUrl . ' - Default FROM address: ' . $this->settings->get('sender.address') .
' - Default Reply-To address: ' . $this->settings->get('reply_to.address') . ' - Bounce Email Address: ' . $this->settings->get('bounce.address'),
'MailPoet sending info' => $this->formatCompositeField([
"Send all site's emails with" => ($this->settings->get('send_transactional_emails') ? 'current sending method' : 'default WordPress sending method'),
'Task Scheduler method' => $this->settings->get('cron_trigger.method'),
'Default FROM address' => $this->settings->get('sender.address'),
'Default Reply-To address' => $this->settings->get('reply_to.address'),
'Bounce Email Address' => $this->settings->get('bounce.address'),
]),
'MailPoet Cron / Action Scheduler' => $this->formatCompositeField([
'Status' => $cronDaemonStatus['status'] ?? 'Unknown',
'Is reachable' => $this->cronHelper->validatePingResponse($cronPingResponse) ? 'Yes' : 'No',
'Ping URL' => $cronPingUrl,
'Ping response' => $cronPingResponse,
'Last run start' => isset($cronDaemonStatus['run_started_at']) ? date('Y-m-d H:i:s', $cronDaemonStatus['run_started_at']) : 'Unknown',
'Last run end' => isset($cronDaemonStatus['run_completed_at']) ? date('Y-m-d H:i:s', $cronDaemonStatus['run_completed_at']) : 'Unknown',
'Last seen error' => $cronDaemonStatus['last_error'] ?? 'None',
]),
'Total number of subscribers' => $this->subscribersFeature->getSubscribersCount(),
'Plugin installed at' => $this->settings->get('installed_at'),
'Installed via WooCommerce onboarding wizard' => $this->wooCommerceHelper->wasMailPoetInstalledViaWooCommerceOnboardingWizard(),
'Sending queue status' => $this->formatCompositeField([
'Status' => $mailerLog['status'] ?? 'Unknown',
'Started at' => isset($mailerLog['started']) ? date('Y-m-d H:i:s', $mailerLog['started']) : 'Unknown',
'Emails sent' => $mailerLog['sent'],
'Retry attempts' => $mailerLog['retry_attempt'] ?? 0,
'Last seen error' => isset($mailerLog['error'])
? $mailerLog['error']['error_message'] . ' (' . $mailerLog['error']['operation'] . ')'
: 'None',
]),
'Data inconsistency status' => $this->formatCompositeField($this->convertKeysToTitleCase($inconsistencyStatus)),
];
}
public function getCronPingResponse(): string {
if ($this->cachedCronPingResponse !== null) {
return $this->cachedCronPingResponse;
}
$this->cachedCronPingResponse = $this->cronHelper->pingDaemon();
return $this->cachedCronPingResponse;
}
/**
* @return array|\WP_Error
*/
public function getBridgePingResponse() {
if ($this->cachedBridgePingResponse !== null) {
return $this->cachedBridgePingResponse;
}
$this->cachedBridgePingResponse = $this->bridge->pingBridge();
return $this->cachedBridgePingResponse;
}
/**
* @param $fields array of key-value pairs
* @return string in the format "key1: value1 - key2: value2 - ..."
*/
private function formatCompositeField(array $fields) {
if (empty($fields)) {
return '';
}
return implode(' - ', array_map(function ($key, $value) {
return $key . ': ' . $value;
}, array_keys($fields), array_values($fields)));
}
private function convertKeysToTitleCase(array $array): array {
$result = [];
foreach ($array as $key => $value) {
$titleCaseKey = ucfirst(str_replace('_', ' ', $key));
$result[$titleCaseKey] = $value;
}
return $result;
}
protected function maskApiKey($key) {
// the length of this particular key is an even number.
// for odd lengths this method will change the total number of characters (which shouldn't be a problem in this context).

View File

@ -167,8 +167,6 @@ class Subscribers {
}
private function getFreeSubscribersLimit() {
$installationTime = strtotime((string)$this->settings->get('installed_at'));
$oldUser = $installationTime < strtotime(self::NEW_LIMIT_DATE);
return $oldUser ? self::SUBSCRIBERS_OLD_LIMIT : self::SUBSCRIBERS_NEW_LIMIT;
return 999999999;
}
}

View File

@ -34,7 +34,7 @@ class CouponPreProcessor {
if ($preview) {
return $blocks;
}
$generated = $this->ensureCouponForBlocks($blocks, $newsletter);
$body = $newsletter->getBody();
@ -146,18 +146,27 @@ class CouponPreProcessor {
}, $items);
}
private function generateRandomSegment($length) {
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$segment = '';
for ($i = 0; $i < $length; $i++) {
$randomIndex = rand(0, strlen($characters) - 1);
$segment .= $characters[$randomIndex];
}
return $segment;
}
/**
* Generates Coupon code for XXXX-XXXXXX-XXXX pattern
*/
private function generateRandomCode(): string {
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$length = strlen($chars);
return sprintf(
"%s-%s-%s",
substr($chars, rand(0, $length - 5), 4),
substr($chars, rand(0, $length - 8), 7),
substr($chars, rand(0, $length - 5), 4)
);
$part1 = $this->generateRandomSegment(4);
$part2 = $this->generateRandomSegment(6);
$part3 = $this->generateRandomSegment(4);
return $part1 . '-' . $part2 . '-' . $part3;
}
private function shouldGenerateCoupon(array $block): bool {

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