Compare commits
152 Commits
5.6.1
...
update-plu
Author | SHA1 | Date | |
---|---|---|---|
c3382d314e | |||
81180caccb | |||
10c82b687d | |||
00c9bf3dcd | |||
27b66e9dc9 | |||
0a166aa9e8 | |||
1727a87e1b | |||
05b7346fe8 | |||
3e68608859 | |||
97ea3f1364 | |||
699c9851d3 | |||
3b9db4f98b | |||
79eca8133a | |||
29fc396df6 | |||
4834863505 | |||
50e986715e | |||
9abf223005 | |||
30719a7840 | |||
0616e9db7b | |||
cb4815f417 | |||
044d203ade | |||
ee8daf137e | |||
b1ed43c333 | |||
076de3b8c6 | |||
e177f3bb36 | |||
c866950d54 | |||
5470b079a1 | |||
5190ac1ff2 | |||
e2286167d8 | |||
2deceb3360 | |||
e058fbd608 | |||
40ad420cfb | |||
dc8699cff4 | |||
da00efc1dc | |||
dade6add4e | |||
8a6ae87fd5 | |||
1464e9b9f6 | |||
bc90aab4dc | |||
145bca48e4 | |||
99dbdb124f | |||
32c7e06152 | |||
7ecb2b5f80 | |||
97be547fdb | |||
ad52971692 | |||
d26fb1b026 | |||
f1abe65e8e | |||
7dfdbef152 | |||
1cb5eda659 | |||
bf5cde8363 | |||
e7169304e0 | |||
00545b3e10 | |||
6a012a8dd6 | |||
a49b978050 | |||
9a2502826f | |||
350a90f872 | |||
1afc0d6260 | |||
0100372027 | |||
78f5791990 | |||
0aba89b89c | |||
849568ac2f | |||
a0ac8862ef | |||
ee6662571f | |||
6127dda6bb | |||
02fc9b2618 | |||
ddf6570f50 | |||
fbe79ae570 | |||
66741b9bc1 | |||
f20237ab1c | |||
bd266ed403 | |||
26ea3a0d04 | |||
a28ff7da50 | |||
ecb040c4a4 | |||
441541e3eb | |||
a41aa8ad54 | |||
94115d0ef2 | |||
c5f94e97b5 | |||
dadf76c519 | |||
14e5a82a49 | |||
edb5a0982c | |||
6ac171bdeb | |||
9b65494bf8 | |||
74f2281ff0 | |||
3d45ea92e2 | |||
6f8a1716c0 | |||
997de285c9 | |||
dc28138da1 | |||
f57fcab0e7 | |||
1b565bf430 | |||
0a02295c1e | |||
b7d6437b77 | |||
23be78a735 | |||
99547cff21 | |||
d7bf0de442 | |||
ffad0ea2e6 | |||
3a87e8a14e | |||
0dd6d64a26 | |||
5ec9e3890e | |||
2c2d6936ba | |||
d7a9062791 | |||
f688ab61a2 | |||
fabc3fd3a4 | |||
0c478691bb | |||
2c7387e781 | |||
019c1a6e44 | |||
58d1a48ab5 | |||
ef4a421eea | |||
9de0e0c137 | |||
17694b3c60 | |||
8b35b447f0 | |||
9120c7da2e | |||
6f396bd22c | |||
08bdf69b52 | |||
2a1e752f63 | |||
78ef34ba8c | |||
74d71a54b2 | |||
cbd8355806 | |||
072c1c0670 | |||
7c887075d6 | |||
adc91d5451 | |||
ce44e07c59 | |||
5680b5d97e | |||
49ccabb5d8 | |||
0d1e141e2f | |||
06532320b5 | |||
d168219ecd | |||
815ff9211b | |||
3300510dc2 | |||
27dbf2f29d | |||
2146323894 | |||
17cb0baa65 | |||
c052130a60 | |||
ce9cfb9a3f | |||
f8fc171c36 | |||
2070b502ad | |||
3693abf0d4 | |||
07629938cb | |||
4059e5cef0 | |||
99d7191a23 | |||
eb03fc9bac | |||
92e9642e1f | |||
3c7b5aba85 | |||
dbe2e6c7e2 | |||
233a4438a5 | |||
e88d0613a6 | |||
9aa900e071 | |||
b2a0f09a7f | |||
142b0f3d2c | |||
7bca42a32e | |||
b98342c295 | |||
8e6c81ae75 | |||
013dd6aad8 | |||
fc7ed37f0d |
@ -197,10 +197,10 @@ jobs:
|
||||
- run:
|
||||
name: Download additional WP Plugins for tests
|
||||
command: |
|
||||
./do download:woo-commerce-zip 9.5.2
|
||||
./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,7 +1082,7 @@ workflows:
|
||||
- acceptance_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: acceptance_oldest
|
||||
woo_core_version: 9.4.3
|
||||
woo_core_version: 9.5.2
|
||||
woo_subscriptions_version: 7.0.0
|
||||
woo_memberships_version: 1.25.2
|
||||
automate_woo_version: 6.0.33
|
||||
@ -1123,7 +1123,7 @@ workflows:
|
||||
- integration_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: integration_oldest
|
||||
woo_core_version: 9.4.3
|
||||
woo_core_version: 9.5.2
|
||||
woo_subscriptions_version: 7.0.0
|
||||
woo_memberships_version: 1.25.2
|
||||
automate_woo_version: 6.0.33
|
||||
@ -1186,7 +1186,7 @@ workflows:
|
||||
- acceptance_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: acceptance_with_premium_oldest
|
||||
woo_core_version: 9.4.3
|
||||
woo_core_version: 9.5.2
|
||||
woo_subscriptions_version: 7.0.0
|
||||
woo_memberships_version: 1.25.2
|
||||
automate_woo_version: 6.0.33
|
||||
@ -1198,7 +1198,7 @@ workflows:
|
||||
- integration_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: integration_with_premium_oldest
|
||||
woo_core_version: 9.4.3
|
||||
woo_core_version: 9.5.2
|
||||
woo_subscriptions_version: 7.0.0
|
||||
woo_memberships_version: 1.25.2
|
||||
automate_woo_version: 6.0.33
|
||||
|
@ -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]
|
||||
|
14
.github/workflows/email-editor-package.yml
vendored
@ -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
@ -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
|
@ -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'
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -143,6 +143,7 @@ export function* activate() {
|
||||
return {
|
||||
type: 'ACTIVATE',
|
||||
automation: data?.data ?? automation,
|
||||
saved: !!data?.data,
|
||||
} as const;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
@ -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,
|
||||
|
@ -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')}
|
||||
/>,
|
||||
|
@ -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"
|
||||
|
@ -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`,
|
||||
|
@ -6,11 +6,18 @@ 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) =>
|
||||
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
|
||||
|
@ -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]);
|
||||
}
|
@ -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('&', '&');
|
||||
},
|
||||
|
||||
plugins: this.options.plugins,
|
||||
|
@ -41,24 +41,36 @@ export const initializeSatismeterSurvey = (writeId = null) => {
|
||||
} 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: {
|
||||
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,
|
||||
},
|
||||
traits,
|
||||
events: {
|
||||
submit: (response) => {
|
||||
if (response.rating >= 9 && response.completed) {
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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'),
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
|
@ -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
@ -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
@ -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.",
|
||||
|
@ -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),
|
||||
]);
|
||||
|
@ -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 = __('Can‘t 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(),
|
||||
|
@ -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),
|
||||
@ -191,6 +190,9 @@ class Reporter {
|
||||
'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'),
|
||||
@ -415,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'],
|
||||
@ -427,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 {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ class AutomationRunLog {
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
public function getId(): ?int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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'),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 === '';
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
@ -351,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);
|
||||
@ -359,6 +361,10 @@ 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);
|
||||
@ -687,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;
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace MailPoet\EmailEditor\Integrations\MailPoet;
|
||||
|
||||
use MailPoet\Analytics\Analytics;
|
||||
use MailPoet\API\JSON\API;
|
||||
use MailPoet\Config\Env;
|
||||
use MailPoet\Config\Installer;
|
||||
use MailPoet\Config\ServicesChecker;
|
||||
@ -11,6 +10,7 @@ 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;
|
||||
@ -74,7 +74,13 @@ class EditorPageRenderer {
|
||||
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();
|
||||
@ -129,25 +135,20 @@ 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()),
|
||||
],
|
||||
]
|
||||
);
|
||||
@ -214,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,
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\EmailEditor\Integrations\MailPoet;
|
||||
|
||||
class MailpoetCssInlinerFactory {
|
||||
public static function create(): MailPoetCssInliner {
|
||||
return new MailPoetCssInliner();
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ 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'];
|
||||
|
||||
|
@ -6,7 +6,7 @@ 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'];
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ 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'];
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
|
@ -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,26 +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);
|
||||
$templateName = $this->templatePrefix . '//' . $newsletter->getSlug();
|
||||
|
||||
if (\WP_Block_Templates_Registry::get_instance()->is_registered($templateName)) {
|
||||
// skip registration if the template was already registered.
|
||||
return;
|
||||
}
|
||||
|
||||
register_block_template(
|
||||
$templateName,
|
||||
[
|
||||
'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;
|
||||
}
|
||||
}
|
||||
|
@ -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 & and in some cases also links with `&amp;` in the database.
|
||||
* This migration fixes the issue by replacing `&amp;` with `& and then & 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;', '&'), '&', '&')
|
||||
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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -216,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 & which is then additionally encoded to &amp;
|
||||
// when saving the text block content in the editor
|
||||
$href = str_replace('&amp;', '&', $anchor->href);
|
||||
// Replace & with & in the href attributes of anchors. URLs are encoded when TinyMCE extracts Text block content via content.innerHTML.
|
||||
// Links containing & 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('&', '&', $href);
|
||||
$anchor->href = $href;
|
||||
}
|
||||
$template = $templateDom->__toString();
|
||||
$template = $this->wp->applyFilters(
|
||||
self::FILTER_POST_PROCESS,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 = __('Can‘t 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).
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
/*
|
||||
* Plugin Name: MailPoet
|
||||
* Version: 5.6.0
|
||||
* Version: 5.7.0
|
||||
* Plugin URI: https://www.mailpoet.com
|
||||
* Description: Create and send newsletters, post notifications and welcome emails from your WordPress.
|
||||
* Author: MailPoet
|
||||
@ -11,8 +11,8 @@
|
||||
* Text Domain: mailpoet
|
||||
* Domain Path: /lang
|
||||
*
|
||||
* WC requires at least: 9.4.3
|
||||
* WC tested up to: 9.5.1
|
||||
* WC requires at least: 9.5.1
|
||||
* WC tested up to: 9.6.0
|
||||
*
|
||||
* @package WordPress
|
||||
* @author MailPoet
|
||||
@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
$mailpoetPlugin = [
|
||||
'version' => '5.6.0',
|
||||
'version' => '5.7.0',
|
||||
'filename' => __FILE__,
|
||||
'path' => dirname(__FILE__),
|
||||
'autoloader' => dirname(__FILE__) . '/vendor/autoload.php',
|
||||
@ -28,7 +28,7 @@ $mailpoetPlugin = [
|
||||
];
|
||||
|
||||
const MAILPOET_MINIMUM_REQUIRED_WP_VERSION = '6.6'; // L-1 version, not the latest
|
||||
const MAILPOET_MINIMUM_REQUIRED_WOOCOMMERCE_VERSION = '9.4'; // L-1 version, not the latest
|
||||
const MAILPOET_MINIMUM_REQUIRED_WOOCOMMERCE_VERSION = '9.5'; // L-1 version, not the latest
|
||||
|
||||
|
||||
// Display WP version error notice
|
||||
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 238 KiB |
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 178 KiB |
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 150 KiB |
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 99 KiB |
@ -14,7 +14,7 @@
|
||||
"doctrine/orm": "2.14.3",
|
||||
"gregwar/captcha": "1.2.1",
|
||||
"monolog/monolog": "2.9.3",
|
||||
"nesbot/carbon": "2.72.5",
|
||||
"nesbot/carbon": "2.72.6",
|
||||
"pelago/emogrifier": "7.2.0",
|
||||
"psr/cache": "^1.0",
|
||||
"sabberworm/php-css-parser": "8.5.1",
|
||||
|
62
mailpoet/prefixer/composer.lock
generated
@ -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": "a6e45b5d5cfd2e18fba27a9fbcdb5eb0",
|
||||
"content-hash": "2acd0a5fe4d1ffb701780b1aa523fb66",
|
||||
"packages": [
|
||||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
@ -1212,16 +1212,16 @@
|
||||
},
|
||||
{
|
||||
"name": "nesbot/carbon",
|
||||
"version": "2.72.5",
|
||||
"version": "2.72.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/briannesbitt/Carbon.git",
|
||||
"reference": "afd46589c216118ecd48ff2b95d77596af1e57ed"
|
||||
"url": "https://github.com/CarbonPHP/carbon.git",
|
||||
"reference": "1e9d50601e7035a4c61441a208cb5bed73e108c5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/afd46589c216118ecd48ff2b95d77596af1e57ed",
|
||||
"reference": "afd46589c216118ecd48ff2b95d77596af1e57ed",
|
||||
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/1e9d50601e7035a4c61441a208cb5bed73e108c5",
|
||||
"reference": "1e9d50601e7035a4c61441a208cb5bed73e108c5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -1241,7 +1241,7 @@
|
||||
"doctrine/orm": "^2.7 || ^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.0",
|
||||
"kylekatarnls/multi-tester": "^2.0",
|
||||
"ondrejmirtes/better-reflection": "*",
|
||||
"ondrejmirtes/better-reflection": "<6",
|
||||
"phpmd/phpmd": "^2.9",
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "^0.12.99 || ^1.7.14",
|
||||
@ -1254,10 +1254,6 @@
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.x-dev",
|
||||
"dev-2.x": "2.x-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Carbon\\Laravel\\ServiceProvider"
|
||||
@ -1267,6 +1263,10 @@
|
||||
"includes": [
|
||||
"extension.neon"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-2.x": "2.x-dev",
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@ -1315,7 +1315,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-03T19:18:41+00:00"
|
||||
"time": "2024-12-27T09:28:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pelago/emogrifier",
|
||||
@ -1965,16 +1965,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v2.5.3",
|
||||
"version": "v2.5.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/deprecation-contracts.git",
|
||||
"reference": "80d075412b557d41002320b96a096ca65aa2c98d"
|
||||
"reference": "605389f2a7e5625f273b53960dc46aeaf9c62918"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d",
|
||||
"reference": "80d075412b557d41002320b96a096ca65aa2c98d",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918",
|
||||
"reference": "605389f2a7e5625f273b53960dc46aeaf9c62918",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -1982,12 +1982,12 @@
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/contracts",
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "2.5-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@ -2012,7 +2012,7 @@
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3"
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -2028,7 +2028,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-01-24T14:02:46+00:00"
|
||||
"time": "2024-09-25T14:11:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
@ -2520,8 +2520,8 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@ -2749,8 +2749,8 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@ -3169,12 +3169,12 @@
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/contracts",
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "2.5-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@ -3511,5 +3511,5 @@
|
||||
"platform-overrides": {
|
||||
"php": "7.4"
|
||||
},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.3.0"
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ Contributors: mailpoet, woocommerce, automattic
|
||||
Tags: email marketing, post notification, woocommerce emails, email automation, newsletter
|
||||
Requires at least: 6.6
|
||||
Tested up to: 6.7
|
||||
Stable tag: 5.6.0
|
||||
Stable tag: 5.7.0
|
||||
Requires PHP: 7.4
|
||||
License: GPLv3
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
@ -105,39 +105,33 @@ Please note:
|
||||
|
||||
= Translations =
|
||||
|
||||
**Official translations**
|
||||
|
||||
* Albanian
|
||||
* Arabic
|
||||
* Catalan
|
||||
* Chinese
|
||||
* Czech
|
||||
* Danish
|
||||
* Dutch
|
||||
* French (FR)
|
||||
* Dutch (Formal)
|
||||
* French (Canada)
|
||||
* French (France)
|
||||
* German
|
||||
* German (Switzerland)
|
||||
* German (Formal)
|
||||
* Greek
|
||||
* Hindi
|
||||
* Italian
|
||||
* Japanese
|
||||
* Mexican
|
||||
* Persian (IR)
|
||||
* Polish
|
||||
* Portuguese (BR and PT)
|
||||
* Portuguese (Brazil)
|
||||
* Portuguese (Portugal)
|
||||
* Romanian
|
||||
* Russian
|
||||
* Serbian
|
||||
* Spanish
|
||||
* Slovak
|
||||
* Spanish (Mexico)
|
||||
* Spanish (Spain)
|
||||
* Swedish
|
||||
* Turkish
|
||||
|
||||
**Community translations**
|
||||
|
||||
* Albanian
|
||||
* British
|
||||
* French (CA)
|
||||
* Hebrew
|
||||
* Hungarian
|
||||
* Norwegian
|
||||
* Persian
|
||||
* Romanian
|
||||
* Ukrainian
|
||||
|
||||
We welcome experienced translators to translate directly on [our Transifex project](https://www.transifex.com/wysija/mp3/). Please note that any translations submitted via the "Translating WordPress" website will not work.
|
||||
@ -224,13 +218,15 @@ Check our [Knowledge Base](https://kb.mailpoet.com) or contact us through our [s
|
||||
3. MailPoet email types
|
||||
4. Newsletter stats (Premium)
|
||||
5. Subscriber import (via a CSV file or directly from MailChimp)
|
||||
6. WooCommerce emails
|
||||
6. Automation editor (Premium)
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 5.6.0 - 2025-01-06 =
|
||||
* Changed: minimum required WooCommerce is 9.4;
|
||||
* Changed: when WordPress or WooCommerce version requirements are not met, MailPoet plugin won't deactivate, and instead stop executing. Once the requirements are met, it will start working automatically;
|
||||
* Fixed: abandoned cart tasks are processed on time.
|
||||
= 5.7.0 - 2025-02-11 =
|
||||
* Improved: Rename Review Trigger in Automations;
|
||||
* Changed: minimum required WooCommerce is 9.5;
|
||||
* Fixed: Automation UI shows wrong saved status after failed activation;
|
||||
* Fixed: Email preview does not work with sent emails;
|
||||
* Fixed: email content patterns are mixed with page starter patterns.
|
||||
|
||||
[See the changelog for all versions.](https://github.com/mailpoet/mailpoet/blob/trunk/mailpoet/CHANGELOG.md)
|
||||
[See the changelog for all versions.](https://github.com/mailpoet/mailpoet/blob/trunk/mailpoet/changelog.txt)
|
||||
|
@ -3,6 +3,7 @@ parameters:
|
||||
tmpDir: ../../temp/phpstan
|
||||
bootstrapFiles:
|
||||
- ../../vendor/autoload.php
|
||||
- ../../../packages/php/email-editor/vendor/autoload.php
|
||||
- vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
|
||||
- ../../../tests_env/vendor/codeception/codeception/autoload.php
|
||||
- ../../../tests_env/vendor/codeception/verify/src/Codeception/Verify/Verify.php
|
||||
|
@ -72,9 +72,9 @@ class ChangelogController {
|
||||
}
|
||||
|
||||
private function updateReadme($heading, $changesList) {
|
||||
if (file_exists(dirname($this->readmeFile) . DIRECTORY_SEPARATOR . 'CHANGELOG.md')) {
|
||||
if (file_exists(dirname($this->readmeFile) . DIRECTORY_SEPARATOR . 'changelog.txt')) {
|
||||
// for the free plugin, in the premium, we don't use the changelog file
|
||||
$this->addChangelogEntryToFile($heading, $changesList, dirname($this->readmeFile) . DIRECTORY_SEPARATOR . 'CHANGELOG.md');
|
||||
$this->addChangelogEntryToFile($heading, $changesList, dirname($this->readmeFile) . DIRECTORY_SEPARATOR . 'changelog.txt');
|
||||
$this->removePreviousChangelogFromReadmeFile();
|
||||
}
|
||||
$this->addChangelogEntryToFile($heading, $changesList, $this->readmeFile);
|
||||
@ -84,11 +84,6 @@ class ChangelogController {
|
||||
$headingPrefix = explode(self::HEADING_GLUE, $heading)[0];
|
||||
$headersDelimiter = "\n";
|
||||
|
||||
if (strpos($fileName, '.md') !== false) {
|
||||
$headersDelimiter .= "\n";
|
||||
$changesList = preg_replace("/^\*/m", "-", $changesList);
|
||||
}
|
||||
|
||||
$fileContents = file_get_contents($fileName);
|
||||
$changelog = "$heading$headersDelimiter$changesList";
|
||||
|
||||
|
@ -113,17 +113,19 @@ class CreateAndSendEmailUsingGutenbergCest {
|
||||
$i->waitForText('Email saved!');
|
||||
// there is weird issue in the acceptance env where preview popup goes beyond sidebar
|
||||
// this issue is not confirmed to be real issue but only on the acceptance test site
|
||||
$i->click('div.interface-pinned-items > button'); // close sidebar
|
||||
$i->click('[aria-label="Close sidebar"]'); // close sidebar
|
||||
$i->click('.mailpoet-preview-dropdown button[aria-label="Preview"]');
|
||||
$i->waitForElementVisible('//button[text()="Preview in new tab"]');
|
||||
$i->waitForElementClickable('//button[text()="Preview in new tab"]');
|
||||
$i->click('//button[text()="Preview in new tab"]');
|
||||
$i->waitForElementVisible('//a[text()="Preview in new tab"]');
|
||||
$i->waitForElementClickable('//a[text()="Preview in new tab"]');
|
||||
$i->click('//a[text()="Preview in new tab"]');
|
||||
$i->switchToNextTab();
|
||||
$i->canSeeInCurrentUrl('endpoint=view_in_browser');
|
||||
$i->canSeeInCurrentUrl('post_type=mailpoet_email');
|
||||
$i->canSee('Sample text');
|
||||
$i->closeTab();
|
||||
|
||||
$i->wantTo('Send preview email and verify it was delivered');
|
||||
$i->click('.mailpoet-preview-dropdown button[aria-label="Preview"]');
|
||||
$i->waitForElementVisible('//span[text()="Send a test email"]');
|
||||
$i->click('//span[text()="Send a test email"]'); // MenuItem component renders a button containing span
|
||||
$i->waitForElementClickable('//button[text()="Send test email"]');
|
||||
$i->click('//button[text()="Send test email"]');
|
||||
|
@ -189,6 +189,12 @@ class WooCheckoutBlocksCest {
|
||||
private function orderProduct(\AcceptanceTester $i, string $userEmail, bool $doRegister = true, bool $doSubscribe = true): void {
|
||||
$i->addProductToCart($this->product);
|
||||
$i->amOnPage("/?p={$this->checkoutPostId}");
|
||||
|
||||
// refresh the page to fix https://github.com/woocommerce/woocommerce/issues/54805
|
||||
// this can be removed after the above is released
|
||||
// should be done in Woo 9.7 (release date 24th Feb 2025)
|
||||
$i->reloadPage();
|
||||
|
||||
$this->fillBlocksCustomerInfo($i, $userEmail);
|
||||
if ($doSubscribe) {
|
||||
$settings = (ContainerWrapper::getInstance())->get(SettingsController::class);
|
||||
|
@ -616,14 +616,12 @@ class ServicesTest extends \MailPoetTest {
|
||||
);
|
||||
}
|
||||
|
||||
public function testItReturnsErrorIfBridgePingThrowsException() {
|
||||
$errorMessage = 'some error';
|
||||
public function testItReturnsErrorIfBridgePingReturnWPError() {
|
||||
$errorMessage = 'cURL error 6: Could not resolve host: https://bridge.mailpoet.com';
|
||||
$bridge = $this->make(
|
||||
new Bridge(),
|
||||
[
|
||||
'pingBridge' => function () use ($errorMessage) {
|
||||
throw new \Exception($errorMessage);
|
||||
},
|
||||
'pingBridge' => new \WP_Error('error', $errorMessage),
|
||||
]
|
||||
);
|
||||
$servicesEndpoint = $this->createServicesEndpointWithMocks(['bridge' => $bridge]);
|
||||
@ -631,21 +629,25 @@ class ServicesTest extends \MailPoetTest {
|
||||
$response = $response->getData();
|
||||
verify($response['errors'][0])->isArray();
|
||||
verify($response['errors'][0]['message'])->stringContainsString($errorMessage);
|
||||
verify($response['errors'][0]['message'])->stringContainsString('Contact your hosting support');
|
||||
verify($response['errors'][0]['error'])->stringContainsString('unknown');
|
||||
}
|
||||
|
||||
public function testItReturnsErrorIfBridgePingResultIsUnsuccessful() {
|
||||
$errorCode = 500;
|
||||
$bridge = $this->make(
|
||||
new Bridge(),
|
||||
[
|
||||
'pingBridge' => false,
|
||||
'pingBridge' => [
|
||||
'response' => ['code' => $errorCode],
|
||||
],
|
||||
]
|
||||
);
|
||||
$servicesEndpoint = $this->createServicesEndpointWithMocks(['bridge' => $bridge]);
|
||||
$response = $servicesEndpoint->pingBridge();
|
||||
$response = $response->getData();
|
||||
verify($response['errors'][0])->isArray();
|
||||
verify($response['errors'][0]['message'])->stringContainsString('Contact your hosting support');
|
||||
verify($response['errors'][0]['message'])->stringContainsString('code: 500');
|
||||
verify($response['errors'][0]['error'])->stringContainsString('unknown');
|
||||
}
|
||||
|
||||
@ -653,7 +655,9 @@ class ServicesTest extends \MailPoetTest {
|
||||
$bridge = $this->make(
|
||||
new Bridge(),
|
||||
[
|
||||
'pingBridge' => 200, // HTTP OK
|
||||
'pingBridge' => [
|
||||
'response' => ['code' => 200],
|
||||
],
|
||||
]
|
||||
);
|
||||
$servicesEndpoint = $this->createServicesEndpointWithMocks(['bridge' => $bridge]);
|
||||
|
@ -5,6 +5,7 @@ namespace MailPoet\Test\Automation\Integrations\Core\Actions;
|
||||
use ActionScheduler_Action;
|
||||
use ActionScheduler_Store;
|
||||
use MailPoet\Automation\Engine\Control\StepRunControllerFactory;
|
||||
use MailPoet\Automation\Engine\Control\StepRunLoggerFactory;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\AutomationRun;
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
@ -96,7 +97,8 @@ class IfElseActionTest extends MailPoetTest {
|
||||
// run
|
||||
$run = new AutomationRun(1, 1, 'trigger-key', [$subject], 1);
|
||||
$args = new StepRunArgs($automation, $run, $step, [new SubjectEntry($this->subscriberSubject, $subject)], 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args);
|
||||
$logger = $this->diContainer->get(StepRunLoggerFactory::class)->createLogger($run->getId(), $step->getId(), $step->getType(), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args, $logger);
|
||||
$this->action->run($args, $controller);
|
||||
|
||||
// check
|
||||
|
@ -7,6 +7,7 @@ use MailPoet\Automation\Engine\Builder\UpdateAutomationController;
|
||||
use MailPoet\Automation\Engine\Control\ActionScheduler;
|
||||
use MailPoet\Automation\Engine\Control\AutomationController;
|
||||
use MailPoet\Automation\Engine\Control\StepRunControllerFactory;
|
||||
use MailPoet\Automation\Engine\Control\StepRunLoggerFactory;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\AutomationRun;
|
||||
use MailPoet\Automation\Engine\Data\NextStep;
|
||||
@ -31,6 +32,7 @@ use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
|
||||
use MailPoet\Segments\SegmentsRepository;
|
||||
use MailPoet\Subscribers\SubscribersRepository;
|
||||
use MailPoet\Test\DataFactories\Automation as AutomationFactory;
|
||||
use MailPoet\Test\DataFactories\AutomationRun as AutomationRunFactory;
|
||||
use MailPoet\Test\DataFactories\Newsletter;
|
||||
use MailPoet\Test\DataFactories\ScheduledTask;
|
||||
use MailPoet\Test\DataFactories\ScheduledTaskSubscriber;
|
||||
@ -129,7 +131,8 @@ class SendEmailActionTest extends \MailPoetTest {
|
||||
|
||||
// first run
|
||||
$args = new StepRunArgs($automation, $run, $step, $this->getSubjectEntries($subjects), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args);
|
||||
$logger = $this->diContainer->get(StepRunLoggerFactory::class)->createLogger($run->getId(), $step->getId(), $step->getType(), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args, $logger);
|
||||
$this->action->run($args, $controller);
|
||||
|
||||
// scheduled task
|
||||
@ -150,7 +153,8 @@ class SendEmailActionTest extends \MailPoetTest {
|
||||
|
||||
// progress — won't throw an exception when the email was sent (= step will be completed)
|
||||
$args = new StepRunArgs($automation, $run, $step, $this->getSubjectEntries($subjects), 2);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args);
|
||||
$logger = $this->diContainer->get(StepRunLoggerFactory::class)->createLogger($run->getId(), $step->getId(), $step->getType(), 2);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args, $logger);
|
||||
$this->action->run($args, $controller);
|
||||
}
|
||||
|
||||
@ -169,7 +173,8 @@ class SendEmailActionTest extends \MailPoetTest {
|
||||
|
||||
// progress run step args
|
||||
$args = new StepRunArgs($automation, $run, $step, $this->getSubjectEntries($subjects), 2);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args);
|
||||
$logger = $this->diContainer->get(StepRunLoggerFactory::class)->createLogger($run->getId(), $step->getId(), $step->getType(), 2);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args, $logger);
|
||||
|
||||
// email was never scheduled
|
||||
$this->assertThrowsExceptionWithMessage(
|
||||
@ -264,7 +269,7 @@ class SendEmailActionTest extends \MailPoetTest {
|
||||
|
||||
$step = new Step('step-id', Step::TYPE_ACTION, 'step-key', ['email_id' => $email->getId()], []);
|
||||
$automation = new Automation('some-automation', [$step->getId() => $step], new \WP_User());
|
||||
$run = new AutomationRun(1, 1, 'trigger-key', $subjects);
|
||||
$run = new AutomationRun(1, 1, 'trigger-key', $subjects, 1);
|
||||
|
||||
$scheduled = $this->scheduledTasksRepository->findByNewsletterAndSubscriberId($email, (int)$subscriber->getId());
|
||||
verify($scheduled)->arrayCount(0);
|
||||
@ -272,7 +277,8 @@ class SendEmailActionTest extends \MailPoetTest {
|
||||
$this->segmentsRepository->bulkDelete([$segment->getId()]);
|
||||
$action = ContainerWrapper::getInstance()->get(SendEmailAction::class);
|
||||
$args = new StepRunArgs($automation, $run, $step, $this->getSubjectEntries($subjects), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args);
|
||||
$logger = $this->diContainer->get(StepRunLoggerFactory::class)->createLogger($run->getId(), $step->getId(), $step->getType(), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args, $logger);
|
||||
|
||||
try {
|
||||
$action->run($args, $controller);
|
||||
@ -295,7 +301,7 @@ class SendEmailActionTest extends \MailPoetTest {
|
||||
|
||||
$step = new Step('step-id', Step::TYPE_ACTION, 'step-key', ['email_id' => $email->getId()], []);
|
||||
$automation = new Automation('some-automation', [$step->getId() => $step], new \WP_User());
|
||||
$run = new AutomationRun(1, 1, 'trigger-key', $subjects);
|
||||
$run = new AutomationRun(1, 1, 'trigger-key', $subjects, 1);
|
||||
|
||||
$scheduled = $this->scheduledTasksRepository->findByNewsletterAndSubscriberId($email, (int)$subscriber->getId());
|
||||
verify($scheduled)->arrayCount(0);
|
||||
@ -303,7 +309,8 @@ class SendEmailActionTest extends \MailPoetTest {
|
||||
$this->subscribersRepository->bulkDelete([$subscriber->getId()]);
|
||||
$action = ContainerWrapper::getInstance()->get(SendEmailAction::class);
|
||||
$args = new StepRunArgs($automation, $run, $step, $this->getSubjectEntries($subjects), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args);
|
||||
$logger = $this->diContainer->get(StepRunLoggerFactory::class)->createLogger($run->getId(), $step->getId(), $step->getType(), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args, $logger);
|
||||
|
||||
try {
|
||||
$action->run($args, $controller);
|
||||
@ -335,7 +342,7 @@ class SendEmailActionTest extends \MailPoetTest {
|
||||
|
||||
$step = new Step('step-id', Step::TYPE_ACTION, 'step-key', ['email_id' => $email->getId()], []);
|
||||
$automation = new Automation('some-automation', [$step->getId() => $step], new \WP_User());
|
||||
$run = new AutomationRun(1, 1, 'trigger-key', $subjects);
|
||||
$run = new AutomationRun(1, 1, 'trigger-key', $subjects, 1);
|
||||
|
||||
$scheduled = $this->scheduledTasksRepository->findByNewsletterAndSubscriberId($email, (int)$subscriber->getId());
|
||||
verify($scheduled)->arrayCount(0);
|
||||
@ -343,7 +350,8 @@ class SendEmailActionTest extends \MailPoetTest {
|
||||
$this->subscribersRepository->bulkDelete([$subscriber->getId()]);
|
||||
$action = ContainerWrapper::getInstance()->get(SendEmailAction::class);
|
||||
$args = new StepRunArgs($automation, $run, $step, $this->getSubjectEntries($subjects), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args);
|
||||
$logger = $this->diContainer->get(StepRunLoggerFactory::class)->createLogger($run->getId(), $step->getId(), $step->getType(), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args, $logger);
|
||||
|
||||
try {
|
||||
$action->run($args, $controller);
|
||||
@ -356,6 +364,98 @@ class SendEmailActionTest extends \MailPoetTest {
|
||||
}
|
||||
}
|
||||
|
||||
public function testRerunForOptinWithDelivery(): void {
|
||||
// Build a newsletter with 1 unconfirmed subscriber.
|
||||
$segment = (new Segment())->create();
|
||||
$subscriber = (new Subscriber())
|
||||
->withStatus(SubscriberEntity::STATUS_UNCONFIRMED)
|
||||
->withSegments([$segment])
|
||||
->create();
|
||||
$newsletter = (new Newsletter())->withAutomationType()->create();
|
||||
$subjects = $this->getSubjectData($subscriber, $segment);
|
||||
|
||||
// Build an automation.
|
||||
$steps = [
|
||||
new Step('root', Step::TYPE_ROOT, 'root', [], [new NextStep('trigger')]),
|
||||
new Step('trigger', Step::TYPE_TRIGGER, 'test-trigger', [], [new NextStep('emailstep')]),
|
||||
new Step('emailstep', Step::TYPE_ACTION, SendEmailAction::KEY, ['email_id' => $newsletter->getId()], []),
|
||||
];
|
||||
$automation = (new AutomationFactory())->withSteps($steps)->create();
|
||||
$run = (new AutomationRunFactory())
|
||||
->withAutomation($automation)
|
||||
->withTriggerKey('trigger-key')
|
||||
->create();
|
||||
|
||||
// Prepare action run.
|
||||
$args = new StepRunArgs($automation, $run, end($steps), $this->getSubjectEntries($subjects), 1);
|
||||
$step = end($steps);
|
||||
$logger = $this->diContainer->get(StepRunLoggerFactory::class)->createLogger($run->getId(), $step->getId(), $step->getType(), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args, $logger);
|
||||
|
||||
// First run: with no opt-in => schedule a retry
|
||||
$actionScheduler = $this->diContainer->get(ActionScheduler::class);
|
||||
$this->assertCount(0, $actionScheduler->getScheduledActions());
|
||||
$this->action->run($args, $controller);
|
||||
$this->assertCount(1, $actionScheduler->getScheduledActions());
|
||||
|
||||
// Second run: opt-in happened, so 1 email should be scheduled.
|
||||
$subscriber->setStatus(SubscriberEntity::STATUS_SUBSCRIBED);
|
||||
$this->subscribersRepository->persist($subscriber);
|
||||
$args = new StepRunArgs($automation, $run, end($steps), $this->getSubjectEntries($subjects), 2);
|
||||
$this->action->run($args, $controller);
|
||||
$scheduled = $this->scheduledTasksRepository->findByNewsletterAndSubscriberId($newsletter, (int)$subscriber->getId());
|
||||
verify($scheduled)->arrayCount(1);
|
||||
$this->assertSame([
|
||||
'automation' => ['id' => $automation->getId(), 'run_id' => $run->getId(), 'step_id' => 'emailstep', 'run_number' => 2],
|
||||
], $scheduled[0]->getMeta());
|
||||
}
|
||||
|
||||
public function testRerunForOptinWithNoRetriesLeft(): void {
|
||||
// Build a newsletter with 1 unconfirmed subscriber.
|
||||
$segment = (new Segment())->create();
|
||||
$subscriber = (new Subscriber())
|
||||
->withStatus(SubscriberEntity::STATUS_UNCONFIRMED)
|
||||
->withSegments([$segment])
|
||||
->create();
|
||||
$newsletter = (new Newsletter())->withAutomationType()->create();
|
||||
$subjects = $this->getSubjectData($subscriber, $segment);
|
||||
|
||||
// Build an automation.
|
||||
$steps = [
|
||||
new Step('root', Step::TYPE_ROOT, 'root', [], [new NextStep('trigger')]),
|
||||
new Step('trigger', Step::TYPE_TRIGGER, 'test-trigger', [], [new NextStep('emailstep')]),
|
||||
new Step('emailstep', Step::TYPE_ACTION, SendEmailAction::KEY, ['email_id' => $newsletter->getId()], []),
|
||||
];
|
||||
$automation = (new AutomationFactory())->withSteps($steps)->create();
|
||||
$run = (new AutomationRunFactory())
|
||||
->withAutomation($automation)
|
||||
->withTriggerKey('trigger-key')
|
||||
->create();
|
||||
|
||||
// Prepare action run.
|
||||
$args = new StepRunArgs($automation, $run, end($steps), $this->getSubjectEntries($subjects), 1);
|
||||
$step = end($steps);
|
||||
$logger = $this->diContainer->get(StepRunLoggerFactory::class)->createLogger($run->getId(), $step->getId(), $step->getType(), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args, $logger);
|
||||
|
||||
// First run: with no opt-in => schedule a retry
|
||||
$actionScheduler = $this->diContainer->get(ActionScheduler::class);
|
||||
$this->assertCount(0, $actionScheduler->getScheduledActions());
|
||||
$this->action->run($args, $controller);
|
||||
$this->assertCount(1, $actionScheduler->getScheduledActions());
|
||||
|
||||
// Nth run: after 6 re-runs without opt-in, we've exhausted retries
|
||||
$args = new StepRunArgs($automation, $run, end($steps), $this->getSubjectEntries($subjects), 7);
|
||||
$this->assertThrowsExceptionWithMessage(
|
||||
"Cannot send the email because the subscriber's status is 'unconfirmed'.",
|
||||
function() use ($args, $controller) {
|
||||
$this->action->run($args, $controller);
|
||||
}
|
||||
);
|
||||
$scheduled = $this->scheduledTasksRepository->findByNewsletterAndSubscriberId($newsletter, (int)$subscriber->getId());
|
||||
verify($scheduled)->arrayCount(0);
|
||||
}
|
||||
|
||||
public function testNothingScheduledIfSubscriberNotSubscribedToSegment(): void {
|
||||
$segment = (new Segment())->create();
|
||||
$subscriber = (new Subscriber())
|
||||
@ -366,14 +466,15 @@ class SendEmailActionTest extends \MailPoetTest {
|
||||
|
||||
$step = new Step('step-id', Step::TYPE_ACTION, 'step-key', ['email_id' => $email->getId()], []);
|
||||
$automation = new Automation('some-automation', [$step->getId() => $step], new \WP_User());
|
||||
$run = new AutomationRun(1, 1, 'trigger-key', $subjects);
|
||||
$run = new AutomationRun(1, 1, 'trigger-key', $subjects, 1);
|
||||
|
||||
$scheduled = $this->scheduledTasksRepository->findByNewsletterAndSubscriberId($email, (int)$subscriber->getId());
|
||||
verify($scheduled)->arrayCount(0);
|
||||
|
||||
$action = ContainerWrapper::getInstance()->get(SendEmailAction::class);
|
||||
$args = new StepRunArgs($automation, $run, $step, $this->getSubjectEntries($subjects), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args);
|
||||
$logger = $this->diContainer->get(StepRunLoggerFactory::class)->createLogger($run->getId(), $step->getId(), $step->getType(), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args, $logger);
|
||||
|
||||
try {
|
||||
$action->run($args, $controller);
|
||||
@ -413,7 +514,8 @@ class SendEmailActionTest extends \MailPoetTest {
|
||||
|
||||
$action = ContainerWrapper::getInstance()->get(SendEmailAction::class);
|
||||
$args = new StepRunArgs($automation, $run, $step, $this->getSubjectEntries($subjects), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args);
|
||||
$logger = $this->diContainer->get(StepRunLoggerFactory::class)->createLogger($run->getId(), $step->getId(), $step->getType(), 1);
|
||||
$controller = $this->diContainer->get(StepRunControllerFactory::class)->createController($args, $logger);
|
||||
|
||||
$action->run($args, $controller);
|
||||
$scheduled = $this->scheduledTasksRepository->findByNewsletterAndSubscriberId($email, (int)$subscriber->getId());
|
||||
|
@ -8,6 +8,7 @@ use MailPoet\Automation\Engine\Data\NextStep;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Data\Subject;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationRunStorage;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\AbandonedCartSubject;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\AbandonedCart\AbandonedCartTrigger;
|
||||
use MailPoet\Cron\Workers\Automations\AbandonedCartWorker;
|
||||
@ -29,6 +30,9 @@ class AbandonedCartTriggerTest extends \MailPoetTest {
|
||||
/** @var AutomationRunStorage */
|
||||
private $automationRunStorage;
|
||||
|
||||
/** @var AutomationStorage */
|
||||
private $automationStorage;
|
||||
|
||||
/** @var int */
|
||||
private $productId;
|
||||
|
||||
@ -44,6 +48,7 @@ class AbandonedCartTriggerTest extends \MailPoetTest {
|
||||
public function _before() {
|
||||
$this->testee = $this->diContainer->get(AbandonedCartTrigger::class);
|
||||
$this->automationRunStorage = $this->diContainer->get(AutomationRunStorage::class);
|
||||
$this->automationStorage = $this->diContainer->get(AutomationStorage::class);
|
||||
$this->tasksRepository = $this->diContainer->get(ScheduledTasksRepository::class);
|
||||
$this->abandonedCartWorker = $this->diContainer->get(AbandonedCartWorker::class);
|
||||
$this->productId = $this->createProduct('abandoned cart trigger test product');
|
||||
@ -113,6 +118,66 @@ class AbandonedCartTriggerTest extends \MailPoetTest {
|
||||
$this->assertSame([$this->productId], $abandonedCartSubject->getArgs()['product_ids']);
|
||||
}
|
||||
|
||||
public function testTasksDoNotGetStuckIfAutomationDisabled() {
|
||||
$wait = 1;
|
||||
$automation = $this->createAutomation($wait);
|
||||
$this->testee->registerHooks();
|
||||
$this->assertEmpty($this->tasksRepository->findFutureScheduledByType(AbandonedCartWorker::TASK_TYPE));
|
||||
|
||||
$expectedScheduledTime = new Carbon();
|
||||
$expectedScheduledTime->addMinutes($wait);
|
||||
|
||||
// Add something to the cart.
|
||||
$this->assertIsString(WC()->cart->add_to_cart($this->productId));
|
||||
$scheduledTasks = $this->tasksRepository->findFutureScheduledByType(AbandonedCartWorker::TASK_TYPE);
|
||||
$this->assertCount(1, $scheduledTasks);
|
||||
$task = current($scheduledTasks);
|
||||
$this->assertInstanceOf(ScheduledTaskEntity::class, $task);
|
||||
|
||||
// Disable automation
|
||||
$automation->setStatus(Automation::STATUS_DEACTIVATING);
|
||||
$this->automationStorage->updateAutomation($automation);
|
||||
|
||||
// When the task gets executed, a run is created.
|
||||
$this->assertTrue($this->abandonedCartWorker->processTaskStrategy($task, 1));
|
||||
|
||||
$oneMinutePassed = $expectedScheduledTime->subMinutes($wait);
|
||||
$task->setCreatedAt($oneMinutePassed->subSeconds(60));
|
||||
$this->assertTrue($this->abandonedCartWorker->processTaskStrategy($task, 1));
|
||||
$runs = $this->automationRunStorage->getAutomationRunsForAutomation($automation);
|
||||
$this->assertCount(0, $runs);
|
||||
}
|
||||
|
||||
public function testTasksDoNotGetStuckIfAutomationSubscribersDeleted() {
|
||||
$wait = 1;
|
||||
$automation = $this->createAutomation($wait);
|
||||
$this->testee->registerHooks();
|
||||
$this->assertEmpty($this->tasksRepository->findFutureScheduledByType(AbandonedCartWorker::TASK_TYPE));
|
||||
|
||||
$expectedScheduledTime = new Carbon();
|
||||
$expectedScheduledTime->addMinutes($wait);
|
||||
|
||||
// Add something to the cart.
|
||||
$this->assertIsString(WC()->cart->add_to_cart($this->productId));
|
||||
|
||||
// Delete subscriber directly from the DB
|
||||
$subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$this->entityManager->getConnection()->executeStatement("TRUNCATE TABLE $subscriberTable");
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Get scheduled task for abandoned cart
|
||||
$scheduledTasks = $this->tasksRepository->findFutureScheduledByType(AbandonedCartWorker::TASK_TYPE);
|
||||
$this->assertCount(1, $scheduledTasks);
|
||||
$task = current($scheduledTasks);
|
||||
$this->assertInstanceOf(ScheduledTaskEntity::class, $task);
|
||||
|
||||
$oneMinutePassed = $expectedScheduledTime->subMinutes($wait);
|
||||
$task->setCreatedAt($oneMinutePassed->subSeconds(60));
|
||||
$this->assertTrue($this->abandonedCartWorker->processTaskStrategy($task, 1));
|
||||
$runs = $this->automationRunStorage->getAutomationRunsForAutomation($automation);
|
||||
$this->assertCount(0, $runs);
|
||||
}
|
||||
|
||||
public function testItCancelsTheRunCreationWhenCartIsEmptied() {
|
||||
$wait = 1;
|
||||
$automation = $this->createAutomation($wait);
|
||||
|
@ -0,0 +1,149 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Test\Automation\Integrations\MailPoet\Triggers;
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\NextStep;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationRunStorage;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysFromATagTrigger;
|
||||
use MailPoet\Test\DataFactories\Automation as AutomationFactory;
|
||||
|
||||
/**
|
||||
* @group woo
|
||||
*/
|
||||
class BuysFromATagTriggerTest extends \MailPoetTest {
|
||||
|
||||
/** @var BuysFromATagTrigger */
|
||||
private $testee;
|
||||
|
||||
/** @var AutomationRunStorage */
|
||||
private $automationRunStorage;
|
||||
|
||||
public function _before() {
|
||||
$this->testee = $this->diContainer->get(BuysFromATagTrigger::class);
|
||||
$this->automationRunStorage = $this->diContainer->get(AutomationRunStorage::class);
|
||||
}
|
||||
|
||||
public function testItDoesRunOnlyOncePerOrder() {
|
||||
|
||||
$tag1 = $this->createProductTag("testItDoesRunOnlyOncePerOrder Tag 1");
|
||||
$tag2 = $this->createProductTag("testItDoesRunOnlyOncePerOrder Tag 2");
|
||||
$product1 = $this->createProduct('product 1', $tag1);
|
||||
$product2 = $this->createProduct('product 2', $tag2);
|
||||
$automation = $this->createAutomation([$tag1], 'completed');
|
||||
$this->assertCount(0, $this->automationRunStorage->getAutomationRunsForAutomation($automation));
|
||||
$order = $this->createOrder([$product1, $product2]);
|
||||
|
||||
$this->testee->registerHooks();
|
||||
$order->set_status('on-hold');
|
||||
$order->save();
|
||||
$this->assertCount(0, $this->automationRunStorage->getAutomationRunsForAutomation($automation));
|
||||
$order->set_status('completed');
|
||||
$order->save();
|
||||
$this->assertCount(1, $this->automationRunStorage->getAutomationRunsForAutomation($automation));
|
||||
|
||||
$order->set_status('on-hold');
|
||||
$order->save();
|
||||
$this->assertCount(1, $this->automationRunStorage->getAutomationRunsForAutomation($automation));
|
||||
$order->set_status('completed');
|
||||
$order->save();
|
||||
$this->assertCount(1, $this->automationRunStorage->getAutomationRunsForAutomation($automation));
|
||||
}
|
||||
|
||||
public function testItDoesRunOnAnyStatus() {
|
||||
|
||||
$tag1 = $this->createProductTag("testItDoesRunOnAnyStatus Tag 1");
|
||||
$tag2 = $this->createProductTag("testItDoesRunOnAnyStatus Tag 2");
|
||||
$product1 = $this->createProduct('product 1', $tag1);
|
||||
$product2 = $this->createProduct('product 2', $tag2);
|
||||
$automation = $this->createAutomation([$tag1], 'any');
|
||||
$this->assertCount(0, $this->automationRunStorage->getAutomationRunsForAutomation($automation));
|
||||
$order = $this->createOrder([$product1, $product2]);
|
||||
|
||||
$this->testee->registerHooks();
|
||||
$order->set_status('on-hold');
|
||||
$order->save();
|
||||
$this->assertCount(1, $this->automationRunStorage->getAutomationRunsForAutomation($automation));
|
||||
}
|
||||
|
||||
public function testItDoesNotRunWhenTagsDoNotMatch() {
|
||||
|
||||
$tag1 = $this->createProductTag("testItDoesNotRunWhenCategoriesDoNotMatch Tag 1");
|
||||
$tag2 = $this->createProductTag("testItDoesNotRunWhenCategoriesDoNotMatch Tag 2");
|
||||
$product1 = $this->createProduct('product 1', $tag1);
|
||||
$automation = $this->createAutomation([$tag2], 'completed');
|
||||
$this->assertCount(0, $this->automationRunStorage->getAutomationRunsForAutomation($automation));
|
||||
$order = $this->createOrder([$product1]);
|
||||
|
||||
$this->testee->registerHooks();
|
||||
$order->set_status('completed');
|
||||
$order->save();
|
||||
$this->assertCount(0, $this->automationRunStorage->getAutomationRunsForAutomation($automation));
|
||||
}
|
||||
|
||||
private function createAutomation($categoryIds, $status): Automation {
|
||||
$trigger = new Step(
|
||||
'trigger',
|
||||
Step::TYPE_TRIGGER,
|
||||
BuysFromATagTrigger::KEY,
|
||||
[
|
||||
'tag_ids' => $categoryIds,
|
||||
'to' => $status,
|
||||
],
|
||||
[new NextStep('action')]
|
||||
);
|
||||
$action = new Step(
|
||||
'action',
|
||||
Step::TYPE_ACTION,
|
||||
'core:delay',
|
||||
[
|
||||
'delay' => 1,
|
||||
'delay_type' => 'MINUTES',
|
||||
],
|
||||
[]
|
||||
);
|
||||
return (new AutomationFactory())
|
||||
->withStatusActive()
|
||||
->withStep($trigger)
|
||||
->withStep($action)
|
||||
->create();
|
||||
}
|
||||
|
||||
private function createProductTag(string $name): int {
|
||||
$term = wp_insert_term($name, 'product_tag');
|
||||
if (is_wp_error($term)) {
|
||||
throw new \RuntimeException("Could not create term: " . $term->get_error_message());
|
||||
}
|
||||
return (int)$term['term_id'];
|
||||
}
|
||||
|
||||
private function createProduct(string $name, int $tag, float $price = 1.99): int {
|
||||
|
||||
$product = new \WC_Product();
|
||||
$product->set_name($name);
|
||||
$product->set_tag_ids([$tag]);
|
||||
$product->set_price((string)$price);
|
||||
$product->save();
|
||||
$this->assertTrue(in_array($tag, $product->get_tag_ids()));
|
||||
return $product->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $productIds
|
||||
* @param string $billingEmail
|
||||
* @return \WC_Order
|
||||
* @throws \WC_Data_Exception
|
||||
*/
|
||||
private function createOrder(array $productIds, string $billingEmail = null): \WC_Order {
|
||||
|
||||
$order = new \WC_Order();
|
||||
$order->set_billing_email($billingEmail ?? uniqid() . '@example.com');
|
||||
foreach ($productIds as $id) {
|
||||
$order->add_product(new \WC_Product($id), 1);
|
||||
}
|
||||
|
||||
$order->save();
|
||||
return $order;
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ class PatternsControllerTest extends \MailPoetTest {
|
||||
$this->assertArrayHasKey('categories', $threeColumnContent);
|
||||
$this->assertEquals('mailpoet/3-column-content', $threeColumnContent['name']);
|
||||
$this->assertStringContainsString('A three-column layout organizes information into sections', $threeColumnContent['content']);
|
||||
$this->assertEquals('3 Column', $threeColumnContent['title']);
|
||||
$this->assertEquals('3 Columns', $threeColumnContent['title']);
|
||||
$this->assertEquals(['email-contents'], $threeColumnContent['categories']);
|
||||
|
||||
$twoColumnContent = array_pop($blockPatterns);
|
||||
@ -34,7 +34,7 @@ class PatternsControllerTest extends \MailPoetTest {
|
||||
$this->assertArrayHasKey('categories', $twoColumnContent);
|
||||
$this->assertEquals('mailpoet/2-column-content', $twoColumnContent['name']);
|
||||
$this->assertStringContainsString('A two-column layout organizes information into sections', $twoColumnContent['content']);
|
||||
$this->assertEquals('2 Column', $twoColumnContent['title']);
|
||||
$this->assertEquals('2 Columns', $twoColumnContent['title']);
|
||||
$this->assertEquals(['email-contents'], $twoColumnContent['categories']);
|
||||
|
||||
$oneColumnContent = array_pop($blockPatterns);
|
||||
|
@ -686,6 +686,41 @@ class RendererTest extends \MailPoetTest {
|
||||
verify(preg_match('/<\!--\[if \!mso\]><\!-- -->\s*<table.*<td class=\"mailpoet\_table\_button\".+<\/td>.*<\/table>\s*<\!--<\!\[endif\]-->/s', $template['html']))->equals(1);
|
||||
}
|
||||
|
||||
public function testItFixesAmpersandsInLinks() {
|
||||
$body = json_decode(
|
||||
(string)file_get_contents(dirname(__FILE__) . '/RendererTestData.json'),
|
||||
true
|
||||
);
|
||||
$this->assertIsArray($body);
|
||||
$links = '<a href="https://example.com?a=1&b=2">Link1</a>'; // Ok link
|
||||
$links .= '<a href="https://example.com?c=1&d=2">Link2</a>'; // Link provided by TinyMCE via node.innerHTML
|
||||
$links .= '<a href="https://example.com?e=1&amp;f=2">Link3</a>'; // Link pasted via smart paste and provided by TinyMCE via node.innerHTML
|
||||
|
||||
$body = [
|
||||
'content' => [
|
||||
'type' => 'container',
|
||||
'blocks' => [[
|
||||
'type' => 'container',
|
||||
'styles' => ['block' => []],
|
||||
'blocks' => [[
|
||||
'type' => 'container',
|
||||
'styles' => ['block' => []],
|
||||
'blocks' => [[
|
||||
'type' => 'text',
|
||||
'text' => '<p>' . $links . '</p>',
|
||||
]],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
];
|
||||
|
||||
$this->newsletter->setBody($body);
|
||||
$template = $this->renderer->render($this->newsletter);
|
||||
$this->assertStringContainsString('https://example.com?a=1&b=2', $template['html']);
|
||||
$this->assertStringContainsString('https://example.com?c=1&d=2', $template['html']);
|
||||
$this->assertStringContainsString('https://example.com?e=1&f=2', $template['html']);
|
||||
}
|
||||
|
||||
// Test case for MAILPOET-3660
|
||||
public function testItRendersPostContentWhenMultipleQuotesInPostTitle() {
|
||||
$postTitle = 'This \"is \'a\" test';
|
||||
|
@ -2,11 +2,16 @@
|
||||
|
||||
namespace MailPoet\Test\SystemReport;
|
||||
|
||||
use MailPoet\Cron\CronHelper;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Mailer\MailerLog;
|
||||
use MailPoet\Services\Bridge;
|
||||
use MailPoet\Settings\SettingsController;
|
||||
use MailPoet\SystemReport\SystemReportCollector;
|
||||
use MailPoet\Test\DataFactories\Subscriber as SubscriberFactory;
|
||||
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;
|
||||
|
||||
class SystemReportCollectorTest extends \MailPoetTest {
|
||||
@ -147,15 +152,15 @@ class SystemReportCollectorTest extends \MailPoetTest {
|
||||
}
|
||||
|
||||
public function testItReturnsCronPingUrl() {
|
||||
verify($this->systemInfoData['MailPoet sending info'])->stringContainsString('&action=ping');
|
||||
verify($this->systemInfoData['MailPoet Cron / Action Scheduler'])->stringContainsString('&action=ping');
|
||||
// cron ping URL should react to custom filters
|
||||
$filter = function($url) {
|
||||
$filter = function ($url) {
|
||||
return str_replace(home_url(), 'http://custom_url/', $url);
|
||||
};
|
||||
$wp = new WPFunctions;
|
||||
$wp->addFilter('mailpoet_cron_request_url', $filter);
|
||||
$systemInfoData = $this->systemInfoData = $this->diContainer->get(SystemReportCollector::class)->getData();
|
||||
verify($systemInfoData['MailPoet sending info'])->stringMatchesRegExp('!http:\/\/custom_url\/!');
|
||||
verify($systemInfoData['MailPoet Cron / Action Scheduler'])->stringMatchesRegExp('!http:\/\/custom_url\/!');
|
||||
$wp->removeFilter('mailpoet_cron_request_url', $filter);
|
||||
}
|
||||
|
||||
@ -178,4 +183,122 @@ class SystemReportCollectorTest extends \MailPoetTest {
|
||||
|
||||
$this->assertSame($expectedResult, $systemInfoData['MailPoet Premium/MSS key']);
|
||||
}
|
||||
|
||||
public function testItReturnsCronStatusDetailsWhenDaemonIsActive() {
|
||||
$cronSettings = [
|
||||
'status' => CronHelper::DAEMON_STATUS_ACTIVE,
|
||||
'run_started_at' => time(),
|
||||
'run_completed_at' => time() + 10,
|
||||
'last_error' => 'Some error',
|
||||
];
|
||||
|
||||
$this->settings->set('cron_daemon', $cronSettings);
|
||||
$systemInfoData = $this->diContainer->get(SystemReportCollector::class)->getData();
|
||||
|
||||
$subjectField = $systemInfoData['MailPoet Cron / Action Scheduler'];
|
||||
verify($subjectField)->stringContainsString('Status: ' . $cronSettings['status']);
|
||||
verify($subjectField)->stringContainsString('Is reachable: Yes');
|
||||
verify($subjectField)->stringContainsString('Ping response: pong');
|
||||
verify($subjectField)->stringContainsString('Last run start: ' . date('Y-m-d H:i:s', $cronSettings['run_started_at']));
|
||||
verify($subjectField)->stringContainsString('Last run end: ' . date('Y-m-d H:i:s', $cronSettings['run_completed_at']));
|
||||
verify($subjectField)->stringContainsString('Last seen error: ' . $cronSettings['last_error']);
|
||||
}
|
||||
|
||||
public function testItReturnsCronStatusDetailsWhenDaemonIsInactive() {
|
||||
$cronSettings = [
|
||||
'status' => CronHelper::DAEMON_STATUS_INACTIVE,
|
||||
];
|
||||
|
||||
$this->settings->set('cron_daemon', $cronSettings);
|
||||
$systemInfoData = $this->diContainer->get(SystemReportCollector::class)->getData();
|
||||
|
||||
$subjectField = $systemInfoData['MailPoet Cron / Action Scheduler'];
|
||||
verify($subjectField)->stringContainsString('Status: ' . $cronSettings['status']);
|
||||
verify($subjectField)->stringContainsString('Last run start: Unknown');
|
||||
verify($subjectField)->stringContainsString('Last run end: Unknown');
|
||||
verify($subjectField)->stringContainsString('Last seen error: None');
|
||||
}
|
||||
|
||||
public function testItReturnsSendingQueueStatus() {
|
||||
$mailerLog = MailerLog::createMailerLog();
|
||||
MailerLog::resumeSending();
|
||||
|
||||
try {
|
||||
MailerLog::processError($operation = 'send', $error = 'email rejected');
|
||||
} catch (\Exception $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
$systemInfoData = $this->diContainer->get(SystemReportCollector::class)->getData();
|
||||
$subjectField = $systemInfoData['Sending queue status'];
|
||||
verify($subjectField)->stringContainsString('Started at: ' . date('Y-m-d')); // ignoring time segment
|
||||
verify($subjectField)->stringContainsString('Retry attempts: 1');
|
||||
verify($subjectField)->stringContainsString("Last seen error: $error ($operation)"); // @phpstan-ignore-line
|
||||
|
||||
MailerLog::pauseSending($mailerLog);
|
||||
$systemInfoData = $this->diContainer->get(SystemReportCollector::class)->getData();
|
||||
$subjectField = $systemInfoData['Sending queue status'];
|
||||
verify($subjectField)->stringContainsString('Status: ' . MailerLog::STATUS_PAUSED);
|
||||
}
|
||||
|
||||
public function testItReturnsDataInconsistencyStatus() {
|
||||
$systemInfoData = $this->diContainer->get(SystemReportCollector::class)->getData();
|
||||
$subjectField = $systemInfoData['Data inconsistency status'];
|
||||
verify($subjectField)->stringContainsString('Orphaned sending tasks: 0');
|
||||
verify($subjectField)->stringContainsString('Orphaned sending task subscribers: 0');
|
||||
verify($subjectField)->stringContainsString('Sending queue without newsletter: 0');
|
||||
verify($subjectField)->stringContainsString('Orphaned subscriptions: 0');
|
||||
verify($subjectField)->stringContainsString('Orphaned links: 0');
|
||||
verify($subjectField)->stringContainsString('Orphaned newsletter posts: 0');
|
||||
}
|
||||
|
||||
public function testItReturnsBridgeStatusForSuccessfulConnection() {
|
||||
if (getenv('WP_TEST_ENABLE_NETWORK_TESTS') !== 'true') $this->markTestSkipped();
|
||||
$systemInfoData = $this->diContainer->get(SystemReportCollector::class)->getData();
|
||||
verify($systemInfoData['MailPoet Sending Service'])->stringContainsString('Is reachable: Yes');
|
||||
verify($systemInfoData['MailPoet Sending Service'])->stringContainsString('Ping response: 200 HTTP status code');
|
||||
}
|
||||
|
||||
public function testItReturnsBridgeStatusForUnsuccessfulConnection() {
|
||||
$bridge = $this->createMock(Bridge::class);
|
||||
$errorMessage = 'cURL error 6: Could not resolve host: local.test';
|
||||
$bridge->method('pingBridge')->willReturn(new \WP_Error('error', $errorMessage));
|
||||
$bridge->method('validateBridgePingResponse')->willReturn(false);
|
||||
|
||||
$systemReporter = $this->createSystemReporterWithMockedBridge($bridge);
|
||||
$systemInfoData = $systemReporter->getData();
|
||||
verify($systemInfoData['MailPoet Sending Service'])->stringContainsString("Is reachable: No");
|
||||
verify($systemInfoData['MailPoet Sending Service'])->stringContainsString('Ping response: ' . $errorMessage);
|
||||
}
|
||||
|
||||
public function testItReturnsMSSKeyState() {
|
||||
$this->settings->set(Bridge::API_KEY_STATE_SETTING_NAME . '.state', Bridge::KEY_VALID);
|
||||
$systemInfoData = $this->diContainer->get(SystemReportCollector::class)->getData();
|
||||
verify($systemInfoData['MailPoet Sending Service'])->stringContainsString('API key state: ' . Bridge::KEY_VALID);
|
||||
}
|
||||
|
||||
public function testItReturnsPremiumKeyState() {
|
||||
$this->settings->set(Bridge::PREMIUM_KEY_STATE_SETTING_NAME . '.state', Bridge::KEY_VALID);
|
||||
$systemInfoData = $this->diContainer->get(SystemReportCollector::class)->getData();
|
||||
verify($systemInfoData['MailPoet Sending Service'])->stringContainsString('Premium key state: ' . Bridge::KEY_VALID);
|
||||
}
|
||||
|
||||
private function createSystemReporterWithMockedBridge($bridge) {
|
||||
$settings = $this->diContainer->get(SettingsController::class);
|
||||
$wp = $this->diContainer->get(WPFunctions::class);
|
||||
$subscribersFeature = $this->diContainer->get(SubscribersFeature::class);
|
||||
$wooCommerceHelper = $this->diContainer->get(WooCommerceHelper::class);
|
||||
$dataInconsistencyController = $this->diContainer->get(DataInconsistencyController::class);
|
||||
$cronHelper = $this->diContainer->get(CronHelper::class);
|
||||
|
||||
return new SystemReportCollector(
|
||||
$settings,
|
||||
$wp,
|
||||
$subscribersFeature,
|
||||
$wooCommerceHelper,
|
||||
$dataInconsistencyController,
|
||||
$bridge,
|
||||
$cronHelper
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="mailpoet_widget_icon">
|
||||
<%= source('newsletter/templates/svg/block-icons/product.svg') %>
|
||||
<%= source('newsletter/templates/svg/block-icons/coupon.svg') %>
|
||||
</div>
|
||||
<div class="mailpoet_widget_title"><%= __('Coupon') %></div>
|
||||
|
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="31" viewBox="0 0 32 31">
|
||||
<path d="M32 9.20566L30.384 13.7177V13.7017C29.552 13.3977 28.736 13.4457 27.936 13.8297C27.136 14.2137 26.576 14.8217 26.272 15.6537C25.968 16.4857 26.016 17.3017 26.384 18.1017C26.768 18.9017 27.376 19.4457 28.224 19.7497V19.7657L26.608 24.2777L2.512 15.6697L4.096 11.2057C4.928 11.5097 5.744 11.4617 6.544 11.0777C7.344 10.7097 7.888 10.1017 8.192 9.26966C8.496 8.43766 8.448 7.62166 8.064 6.82166C7.696 6.03766 7.088 5.49366 6.256 5.18966L7.888 0.597656L32 9.20566ZM24.048 18.3097L26.24 12.2937C26.432 11.7977 26.4 11.2537 26.176 10.7737C25.952 10.2937 25.552 9.92566 25.056 9.73366L13.024 5.36566C12 4.99766 10.832 5.55766 10.464 6.54966L8.272 12.5657C7.888 13.6057 8.432 14.7577 9.456 15.1257L21.488 19.5097C21.712 19.5897 21.936 19.6377 22.176 19.6377C23.008 19.6377 23.776 19.1097 24.048 18.3097V18.3097ZM12.752 6.11766L24.768 10.4857C25.072 10.5977 25.312 10.8217 25.456 11.1097C25.584 11.3977 25.6 11.7177 25.488 12.0217L23.296 18.0377C23.088 18.6457 22.368 18.9817 21.76 18.7577L9.744 14.3737C9.12 14.1497 8.8 13.4617 9.024 12.8377L11.216 6.82166C11.376 6.35766 11.84 6.03766 12.336 6.03766C12.48 6.03766 12.608 6.06966 12.752 6.11766V6.11766ZM23.664 25.5417C24.224 25.9737 24.864 26.1977 25.584 26.1977H25.6V30.9977H0V26.2617C0.88 26.2617 1.648 25.9417 2.256 25.3177C2.88 24.7097 3.2 23.9417 3.2 23.0617C3.2 22.1817 2.88 21.4297 2.256 20.8057C1.632 20.1817 0.88 19.8617 0 19.8617V14.9977H1.68L1.232 16.2777L5.824 17.9097C5.008 18.1657 4.4 18.9017 4.4 19.7977V26.1977C4.4 27.3017 5.296 28.1977 6.4 28.1977H19.2C20.304 28.1977 21.2 27.3017 21.2 26.1977V23.3977L22.528 23.8777C22.72 24.5657 23.104 25.1257 23.664 25.5417ZM5.2 26.1977V19.7977C5.2 19.1417 5.744 18.5977 6.4 18.5977H7.728L20.4 23.1257V26.1977C20.4 26.8537 19.856 27.3977 19.2 27.3977H6.4C5.744 27.3977 5.2 26.8537 5.2 26.1977Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
@ -1,5 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="320 237.161 844.61 844.73" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M712.312 495.688c1.823-3.419 2.506-6.838 2.278-10.94-.455-5.245-2.736-9.575-7.066-12.994-4.331-3.42-9.117-5.016-14.361-4.562-6.611.458-11.627 3.65-15.044 10.033-14.134 25.757-24.165 67.475-30.092 125.374-8.661-21.883-15.956-47.644-21.655-77.961-2.508-13.449-8.661-19.83-18.691-19.147-6.839.455-12.54 5.015-17.101 13.678l-49.921 95.058c-8.206-33.052-15.956-73.401-23.022-121.045-1.595-11.856-8.206-17.326-19.833-16.411-6.383.455-11.169 2.732-14.361 7.065-3.191 4.101-4.559 9.346-3.645 15.273 13.448 85.481 25.986 143.157 37.611 173.02 4.559 10.939 9.8 16.184 15.956 15.728 9.575-.684 20.972-13.904 34.421-39.665 7.067-14.589 18.009-36.473 32.827-65.65 12.309 43.084 29.178 75.453 50.378 97.111 5.927 6.154 12.081 8.889 18.009 8.433 5.244-.455 9.345-3.191 12.083-8.206 2.278-4.332 3.191-9.349 2.733-15.045-1.366-20.743.686-49.695 6.382-86.854 5.926-38.296 13.222-65.879 22.114-82.293zM984.263 564.531c.229-17.779-3.646-32.599-10.941-44.907-8.205-14.361-20.289-23.022-36.471-26.443-4.333-.912-8.436-1.367-12.312-1.367-21.885 0-39.663 11.396-53.569 34.193-11.855 19.376-17.78 40.804-17.78 64.284 0 17.552 3.646 32.596 10.94 45.135 8.207 14.36 20.289 23.022 36.473 26.442 4.331.913 8.434 1.369 12.311 1.369 22.111 0 39.892-11.397 53.57-34.194 11.852-19.605 17.779-41.032 17.779-64.512zM944.6 582.766v.002c-3.191 15.044-8.891 26.215-17.327 33.736-6.609 5.929-12.764 8.435-18.462 7.294-5.473-1.139-10.032-5.925-13.45-14.815-2.738-7.066-4.103-14.134-4.103-20.745 0-5.697.455-11.397 1.595-16.639 2.053-9.348 5.928-18.467 12.081-27.127 7.523-11.172 15.503-15.728 23.708-14.134 5.472 1.139 10.032 5.927 13.45 14.817 2.735 7.065 4.103 14.132 4.103 20.743 0 5.928-.455 11.625-1.595 16.868zM794.148 493.181c-4.33-.912-8.433-1.367-12.311-1.367-21.883 0-39.664 11.396-53.568 34.193-11.854 19.376-17.781 40.804-17.781 64.284 0 17.552 3.647 32.596 10.941 45.135 8.206 14.36 20.289 23.022 36.472 26.442 4.333.913 8.435 1.369 12.312 1.369 22.111 0 39.892-11.397 53.569-34.194 11.855-19.605 17.78-41.032 17.78-64.512 0-17.779-3.646-32.599-10.941-44.907-8.207-14.361-20.517-23.022-36.473-26.443zm7.522 89.585v.002c-3.192 15.044-8.893 26.215-17.325 33.736-6.61 5.929-12.766 8.435-18.465 7.294-5.471-1.139-10.029-5.925-13.451-14.815-2.733-7.066-4.1-14.134-4.1-20.745 0-5.697.454-11.397 1.594-16.639 2.053-9.348 5.928-18.467 12.083-27.127 7.522-11.172 15.501-15.728 23.706-14.134 5.472 1.139 10.032 5.927 13.45 14.817 2.737 7.065 4.103 14.132 4.103 20.743.229 5.928-.456 11.625-1.595 16.868z"/>
|
||||
<path d="M 320.95 238.875 L 320.95 1080.766 L 882.21 1080.766 L 1162.84 800.135 L 1162.84 238.875 L 320.95 238.875 Z M 828.787 784.994 L 729.974 729.973 L 513.781 729.973 C 484.895 729.973 461.511 706.59 461.511 677.702 L 461.511 503.464 C 461.281 474.805 484.662 451.192 513.553 451.192 L 970.01 451.192 C 998.899 451.192 1022.282 474.576 1022.282 503.464 L 1022.282 677.701 C 1022.282 706.59 998.898 729.972 970.01 729.972 L 806.32 729.972 L 828.787 784.994 Z M 882.21 1010.608 L 882.21 800.135 L 1092.684 800.135 L 882.21 1010.608 Z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="22" viewBox="0 0 30 22">
|
||||
<path d="M29.4055 0.402344V3.60234H0.605469V0.402344H29.4055ZM2.20547 5.20234H27.8055V21.2023H2.20547V5.20234ZM19.8055 10.0023V8.40234H10.2055V10.0023H19.8055Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 256 B |
@ -109,7 +109,7 @@
|
||||
'libs3rdPartyDescription': __('E.g. Google Fonts in the Form and Email editor and DocsBot to get help. When disabled, you can still reach support at [link]www.mailpoet.com/support/[/link].'),
|
||||
'shareDataTitle': __('Share anonymous data'),
|
||||
'shareDataDescription': __('Share anonymous data and help us improve the plugin. We appreciate your help!'),
|
||||
'captchaTitle': __('Protect your forms against spam signups'),
|
||||
'captchaTitle': __('Protect your MailPoet forms against spam signups'),
|
||||
'captchaDescription': __('Built-in CAPTCHA protects your subscription forms against bots. Alternatively, use reCAPTCHA by Google.'),
|
||||
'reCaptchaDescription': __('Please use the same reCAPTCHA type as in Google reCAPTCHA Settings.'),
|
||||
'signupForCaptchaKey': __('Sign up for an API key pair here.'),
|
||||
@ -120,6 +120,9 @@
|
||||
'yourReCaptchaKey': __('Your reCAPTCHA Site Key'),
|
||||
'yourReCaptchaSecret': __('Your reCAPTCHA Secret Key'),
|
||||
'fillReCaptchaKeys': __('Please fill the reCAPTCHA keys.'),
|
||||
'captchaOnRegisterTitle': __('Protect registration forms'),
|
||||
'captchaOnRegisterWooInactiveDescription': __('Protect WordPress registration pages with a CAPTCHA.'),
|
||||
'captchaOnRegisterWooActiveDescription': __('Protect WordPress and WooCommerce registration pages with a CAPTCHA.'),
|
||||
'disable': __('Disable'),
|
||||
'recalculateSubscribersScoreTitle': __('Recalculate Subscriber Scores'),
|
||||
'recalculateSubscribersScoreDescription': __('MailPoet will recalculate subscriber engagement scores for all subscribers. This may take some time to complete.'),
|
||||
|
@ -3,7 +3,7 @@
|
||||
var mailpoet_logo_url = '<%= cdn_url('welcome-wizard/mailpoet-logo.20200623.png') %>';
|
||||
var wizard_sender_illustration_url = '<%= cdn_url('welcome-wizard/sender.20200623.png') %>';
|
||||
var wizard_tracking_illustration_url = '<%= cdn_url('welcome-wizard/tracking.20200623.png') %>';
|
||||
var wizard_woocommerce_illustration_url = '<%= cdn_url('welcome-wizard/woocommerce.20200623.png') %>';
|
||||
var wizard_woocommerce_illustration_url = '<%= cdn_url('welcome-wizard/woocommerce.20241219.png') %>';
|
||||
var wizard_MSS_pitch_illustration_url = '<%= cdn_url('welcome-wizard/illu-pitch-mss.20190912.png') %>';
|
||||
var finish_wizard_url = '<%= finish_wizard_url %>';
|
||||
var admin_email = <%= json_encode(admin_email) %>;
|
||||
|
@ -3,7 +3,7 @@
|
||||
<% block content %>
|
||||
<script>
|
||||
var mailpoet_logo_url = '<%= cdn_url('welcome-wizard/mailpoet-logo.20200623.png') %>';
|
||||
var wizard_woocommerce_illustration_url = '<%= cdn_url('welcome-wizard/woocommerce.20200623.png') %>';
|
||||
var wizard_woocommerce_illustration_url = '<%= cdn_url('welcome-wizard/woocommerce.20241219.png') %>';
|
||||
var mailpoet_show_customers_import = <%= json_encode(show_customers_import) %>;
|
||||
var finish_wizard_url = '<%= finish_wizard_url %>';
|
||||
</script>
|
||||
|
18
packages/js/email-editor/.eslintrc.js
Normal file
@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
extends: [ 'plugin:@woocommerce/eslint-plugin/recommended' ],
|
||||
overrides: [
|
||||
{
|
||||
files: [ '**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx' ],
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@wordpress/no-unsafe-wp-apis': 'off',
|
||||
'@wordpress/i18n-text-domain': [
|
||||
'error',
|
||||
{
|
||||
allowedTextDomain: [ 'mailpoet' ],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
@ -4,6 +4,8 @@ This folder contains the code for the MailPoet Email Editor JS Package.
|
||||
We aim to extract the package as an independent library, so it can be used in other projects.
|
||||
As we are still in an exploration phase, we keep it together with the MailPoet codebase.
|
||||
|
||||
You can try the email editor in [the WordPress Playground](https://playground.wordpress.net/?mode=seamless#%7B%22preferredVersions%22:%7B%22php%22:%228.2%22,%22wp%22:%22latest%22%7D,%22phpExtensionBundles%22:%5B%22kitchen-sink%22%5D,%22features%22:%7B%7D,%22landingPage%22:%22/wp-admin/admin.php?page=mailpoet-newsletters%22,%22steps%22:%5B%7B%22step%22:%22login%22,%22username%22:%22admin%22,%22password%22:%22password%22%7D,%7B%22step%22:%22installPlugin%22,%22pluginData%22:%7B%22resource%22:%22url%22,%22url%22:%22https://account.mailpoet.com/playground/plugin-proxy/branch:trunk%22%7D%7D,%7B%22step%22:%22mkdir%22,%22path%22:%22wordpress/wp-content/mu-plugins%22%7D,%7B%22step%22:%22writeFile%22,%22path%22:%22wordpress/wp-content/mu-plugins/addFilter-2.php%22,%22data%22:%22%3C?php%20%5Cnuse%20MailPoet%5C%5CDI%5C%5CContainerWrapper;%5Cnuse%20MailPoet%5C%5CFeatures%5C%5CFeatureFlagsRepository;%5Cnuse%20MailPoet%5C%5CFeatures%5C%5CFeaturesController;%5Cnadd_filter('mailpoet_skip_welcome_wizard',%20'__return_true');%22%7D%5D%7D).
|
||||
|
||||
You can locate the PHP package here `packages/php/email-editor`
|
||||
|
||||
## Workflow Commands
|
||||
@ -30,6 +32,15 @@ pnpm run format # runs prettier on files. This uses
|
||||
**Renderer** – responsible for converting saved HTML from Gutenberg editor to HTML for email clients.
|
||||
**Theme Controller** – The theme controller is used to generate settings and styles for the editor. We can define which features for working with content are available in settings. The styles are also used in the Render.
|
||||
|
||||
### Dependencies
|
||||
|
||||
#### Rich-text
|
||||
|
||||
The **Personalization tags** feature relies on the `@wordpress/rich-text` package, which is included in both the Gutenberg plugin and WordPress core.
|
||||
To ensure the correct functionality of the Email Editor and its features, you must use **at least version 7.14.0** of the `@wordpress/rich-text` package.
|
||||
The required minimum version of this package is stored in the assets directory.
|
||||
If your WordPress installation does not use the Gutenberg plugin or does not include the required version, replace the existing `@wordpress/rich-text` package with the one provided in the assets directory.
|
||||
|
||||
### Email Editor
|
||||
* Bootstrapped in the plugin in the [email editor controller](https://github.com/mailpoet/mailpoet/blob/13bf305aeb29bbadd0695ee02a3735e62cc4f21f/mailpoet/lib/EmailEditor/Integrations/MailPoet/EmailEditor.php)
|
||||
* **Components folder** - basically the whole UI of the editor. Most of Gutenberg’s blocks magic happens in block-editor folder.
|
||||
@ -71,3 +82,27 @@ pnpm run format # runs prettier on files. This uses
|
||||
| Orange.fr | iOS/Android | Latest | ? | -/0.07 | No | |
|
||||
| Thunderbird | Windows, macOS, Linux | Latest | Gecko | -/0.61 | Yes | It uses bundled rendering engine so it should be enough to test on one platform |
|
||||
| Windows Mail | Windows | 10, 11 | Word | -/- | Yes | Default Client in Windows. Market share should be over 6% in desktop clients |
|
||||
|
||||
## Actions and Filters
|
||||
|
||||
These actions and filters are currently **Work-in-progress**.
|
||||
We may add, update and delete any of them.
|
||||
|
||||
**Please use with caution**.
|
||||
|
||||
### Actions
|
||||
|
||||
| Name | Argument | Description |
|
||||
|--------------------------------|--------------------|---------------------|
|
||||
| `mailpoet_email_editor_events` | `EventData.detail` | Email editor events |
|
||||
|
||||
### Filters
|
||||
|
||||
| Name | Argument | Return | Description |
|
||||
|--------------------------------------------------|---------------------------|--------------------------------------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| `mailpoet_email_editor_events_tracking_enabled` | `boolean` (false-default) | `boolean` | Used to enable the email editor events tracking and collection |
|
||||
| `mailpoet_email_editor_wrap_editor_component` | `JSX.Element` Editor | `JSX.Element` Editor | The main editor component. Custom component can wrap the editor and provide additional functionality |
|
||||
| `mailpoet_email_editor_send_button_label` | `string` 'Send' | `string` 'Send' (default) | Email editor send button label. The `Send` text can be updated using this filter |
|
||||
| `mailpoet_email_editor_send_action_callback` | `function` sendAction | `function` sendAction | Action to perform when the Send button is clicked |
|
||||
| `mailpoet_email_editor_content_validation_rules` | `array` rules | `EmailContentValidationRule[]` rules | Email editor content validation rules. The validation is done on `send btton` click and revalidated on `save draft` |
|
||||
|
||||
|
1
packages/js/email-editor/assets/rich-text.asset.php
Normal file
@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('wp-a11y', 'wp-compose', 'wp-data', 'wp-deprecated', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-keycodes'), 'version' => '6249fa45d0f64d6a0eda');
|