Compare commits
252 Commits
Author | SHA1 | Date | |
---|---|---|---|
30b7dfe603 | |||
67455a0752 | |||
7ca607f49f | |||
b3aff28335 | |||
489fa9552b | |||
d49040a11e | |||
64f008e106 | |||
2f0d7137db | |||
68bd3314d9 | |||
2083adc3d2 | |||
a4e8bf9a9c | |||
7daa0c9dff | |||
027f3fbefe | |||
e3c4728529 | |||
06f231f4b1 | |||
4966d45d5e | |||
ae38774f85 | |||
b907ca491b | |||
abfc33002c | |||
94954c1601 | |||
914b5752cd | |||
102da43d05 | |||
6cf9f7f1d6 | |||
389017c3b9 | |||
0b4d18faa0 | |||
623010a644 | |||
9e62501c30 | |||
95ec3d2fb9 | |||
f60ae7a8ef | |||
eef47c5bfb | |||
a91108f52e | |||
f0772cc793 | |||
fe20687ae8 | |||
da5051f758 | |||
a9d8a2b164 | |||
c6846a2d4c | |||
81ffe6e4f9 | |||
ab3d27569f | |||
337dcb603f | |||
084022010b | |||
b83dac2ed9 | |||
ddf4c9109c | |||
40cdb9a766 | |||
9cf70c6c3b | |||
333b121b2a | |||
1de328abaa | |||
6b4b1dfcbe | |||
66dc6e67ab | |||
d8d859d209 | |||
4bd7dd4ad6 | |||
359f134881 | |||
a65866d2a8 | |||
3917a559f7 | |||
24eab9aac3 | |||
bb240d8e68 | |||
f3bf4b36e9 | |||
d8729ef43c | |||
4072daa91f | |||
3ae281fc1f | |||
86ce19085a | |||
0aceab5a2e | |||
a48b725c7d | |||
cf5718122f | |||
e7e4276bf2 | |||
551d68ff69 | |||
61fa215607 | |||
89df50c160 | |||
e9326e8c9e | |||
956d693454 | |||
a76fb6c63a | |||
d061d451a4 | |||
52e19d445a | |||
c6a3e08c34 | |||
a11a462eee | |||
310d689219 | |||
118cc83cc2 | |||
3da2144ead | |||
b17a9cb4ae | |||
351c8a0bd7 | |||
c705862e82 | |||
dd2f100acd | |||
88398af343 | |||
28bc4d6943 | |||
e1e690ad14 | |||
110b67bd9c | |||
8788e299e1 | |||
ed81ae1ccd | |||
d19e297d37 | |||
c4ce4fd10b | |||
527ebafc5c | |||
e7f1325d3e | |||
d7d3095824 | |||
d9b103891b | |||
fe8f90a4d8 | |||
6d7bece8d6 | |||
199f3e21e7 | |||
7e9caeffbf | |||
a075b7bd68 | |||
359f734024 | |||
a069ae6884 | |||
6de8b0ceec | |||
edf34ccc2c | |||
57f953933d | |||
9b2e6208e0 | |||
314327aed2 | |||
ce370c76a3 | |||
153d29b167 | |||
7858d4ada2 | |||
adc158367d | |||
69e67f3c7a | |||
4f831f8b17 | |||
36c95f80fd | |||
2fb36e57f9 | |||
401501b97b | |||
d585c27f6e | |||
0775ce2669 | |||
37bb5fd824 | |||
fee5af2ffa | |||
21d03c5bf7 | |||
0854b1ce36 | |||
949c962fc9 | |||
17c0a76754 | |||
d449f02883 | |||
b40c5a5dfe | |||
08aea80a55 | |||
86a4347908 | |||
f242e847bb | |||
3c89bf2f0d | |||
10ea5ec55b | |||
805804f3d3 | |||
74549665d6 | |||
5b78ea9a69 | |||
b25c8b8965 | |||
da5b0ede16 | |||
29de3e2bae | |||
79bc684312 | |||
7bc71429eb | |||
967988519b | |||
3b5a96a3f7 | |||
83b14013ec | |||
cd9904de7d | |||
d305498613 | |||
69833557e4 | |||
cd596245ce | |||
7d36d59e65 | |||
efaa5073fb | |||
37f4082210 | |||
f324abd1d0 | |||
5d56a0368a | |||
106db48f8d | |||
ed2ecb7604 | |||
10dcc4f45d | |||
3237351450 | |||
8d57e81b99 | |||
e37daa6c66 | |||
ad4247a241 | |||
8a4f5c13da | |||
1161e6f3f6 | |||
e5d5e20efd | |||
9fcb9afa9d | |||
b24b7b86fd | |||
ddca94891d | |||
9f14f3cc08 | |||
6b3fc309cc | |||
60d933b39a | |||
ea57fd9c11 | |||
a9788b04d4 | |||
46c45d9bef | |||
3282e2f063 | |||
05e941e449 | |||
6a14a3f7b1 | |||
b858f1159a | |||
f3d73aae03 | |||
494723c818 | |||
93af9d491d | |||
b07d34ee23 | |||
4b2b94db9e | |||
77b9cea62c | |||
e2dc137b59 | |||
2da4f5f3b9 | |||
7549ed7f0f | |||
0719f3c4e3 | |||
ba055b4278 | |||
0b5a809883 | |||
a4f7a05bff | |||
1730578a23 | |||
f88623e48d | |||
46fdd8eeb3 | |||
40f4216ff8 | |||
fe536fcdd0 | |||
62cff7b388 | |||
b24beb1dae | |||
b52c53f7f5 | |||
f66be1b947 | |||
5d3b26bb58 | |||
55b64d0354 | |||
7ce1b6eb6c | |||
97b42a4a91 | |||
bedde323bd | |||
0cf2787937 | |||
f0bc53766b | |||
f7fc2c16c1 | |||
f6d80b6e8b | |||
17d3f66316 | |||
23e08ecb44 | |||
4dfdfc8423 | |||
5cdce529b5 | |||
522b8a87db | |||
6e8660dcc4 | |||
936b7fed74 | |||
d87794aa1f | |||
8675aaef6e | |||
824e097639 | |||
dce8e94f8e | |||
e22ffcb5e3 | |||
8f94096cdd | |||
faa17b0d5c | |||
c4d4a6e594 | |||
9937bcc2d5 | |||
031c7d9866 | |||
0e84ddb957 | |||
5672823472 | |||
d03e11f938 | |||
3dbd91bfef | |||
d0ff2a9eae | |||
78314944aa | |||
872bf07b25 | |||
17b79ee29f | |||
f5b411e2ae | |||
e3e865eac5 | |||
6b7ffbc4ad | |||
4ddcc14eee | |||
7bca78e268 | |||
82159dd4c5 | |||
1e76b214ea | |||
8c44364dab | |||
7aea3528c4 | |||
b7809608d1 | |||
19bb957ae7 | |||
6e5ba84aab | |||
1a20f53916 | |||
57d364d142 | |||
89f76f67ca | |||
5e589cd5e3 | |||
139fb2f1ad | |||
5e7ece1e87 | |||
47a4e3b4e9 | |||
0019ad110e | |||
62325dc096 | |||
290aceb0e9 | |||
a9f4e47fe0 | |||
ede135c2b4 |
@ -524,6 +524,12 @@ jobs:
|
||||
name: 'Pull test docker images'
|
||||
# Pull docker images with 3 retries
|
||||
command: i='0';while ! docker-compose -f tests/docker/docker-compose.yml pull && ((i < 3)); do sleep 3 && i=$[$i+1]; done
|
||||
- run:
|
||||
name: Create docker containers for test
|
||||
# We experienced some failures when creating containers so we do it explicitly with one retry
|
||||
command: |
|
||||
cd tests/docker
|
||||
docker-compose create || docker-compose create
|
||||
- run:
|
||||
name: 'PHP Integration tests'
|
||||
command: |
|
||||
@ -765,7 +771,7 @@ workflows:
|
||||
mysql_command: --max_allowed_packet=100M --default-storage-engine=MYISAM
|
||||
mysql_image: mysql:5.5
|
||||
codeception_image_version: 7.4-cli_20220605.0
|
||||
wordpress_image_version: wp-5.8_php7.3_20221104.1
|
||||
wordpress_image_version: wp-5.9_php7.3_20230213.1
|
||||
requires:
|
||||
- build
|
||||
- unit_tests:
|
||||
|
@ -297,6 +297,29 @@ class RoboFile extends \Robo\Tasks {
|
||||
return $this->runTestsInContainer($opts);
|
||||
}
|
||||
|
||||
public function testPerformance($path = null, $opts = ['url' => null, 'head' => false, 'scenario' => null]) {
|
||||
// run WordPress setup
|
||||
$this->taskExec('COMPOSE_HTTP_TIMEOUT=200 docker-compose run --rm -it setup')
|
||||
->dir(__DIR__ . '/tests/performance')
|
||||
->run();
|
||||
|
||||
// run performance tests
|
||||
$dir = __DIR__;
|
||||
return $this->taskExec("php $dir/tools/xk6browser.php")
|
||||
->arg('run')
|
||||
->option('env', 'URL=' . $opts['url'])
|
||||
->option('env', 'HEADLESS=' . ($opts['head'] ? 'false' : 'true'))
|
||||
->option('env', 'SCENARIO=' . $opts['scenario'])
|
||||
->arg($path ?? "$dir/tests/performance/scenarios.js")
|
||||
->dir($dir)->run();
|
||||
}
|
||||
|
||||
public function testPerformanceClean() {
|
||||
$this->taskExec('COMPOSE_HTTP_TIMEOUT=200 docker-compose down --remove-orphans -v')
|
||||
->dir(__DIR__ . '/tests/performance')
|
||||
->run();
|
||||
}
|
||||
|
||||
public function testAcceptanceMultisite($opts = ['file' => null, 'skip-deps' => false, 'group' => null, 'timeout' => null, 'enable-cot' => false, 'enable-cot-sync' => false]) {
|
||||
return $this->runTestsInContainer(array_merge($opts, ['multisite' => true]));
|
||||
}
|
||||
|
@ -34,7 +34,7 @@
|
||||
.mailpoet-automation-field__error {
|
||||
position: relative;
|
||||
|
||||
input,
|
||||
input:not([type='radio'])
|
||||
select,
|
||||
textarea,
|
||||
input[type='text'].components-form-token-field__input {
|
||||
|
@ -5,6 +5,10 @@
|
||||
.components-panel__body-title.mailpoet-automation-panel-plain-body-title {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-automation-panel-plain-body-title-text {
|
||||
|
@ -111,10 +111,29 @@ h2 {
|
||||
|
||||
.edit-post-visual-editor {
|
||||
background-color: $color-white;
|
||||
padding: 10px;
|
||||
padding: 10px 10px 100px;
|
||||
}
|
||||
|
||||
// Unify padding o wp-block-columns with background with front end rendering
|
||||
.wp-block-columns.has-background {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
// Close button animation
|
||||
.edit-post-header-toolbar.edit-post-header-toolbar__left > .edit-post-header-toolbar__inserter-toggle {
|
||||
svg {
|
||||
transition: transform cubic-bezier(.165, .84, .44, 1) .2s;
|
||||
}
|
||||
|
||||
&.is-pressed svg {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Hide block selector header with close button on desktops
|
||||
|
||||
@include respond-to(not-small-screen) {
|
||||
.edit-post-editor__inserter-panel-header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
// Override CSS for HelpScout beacon on form editor page
|
||||
.admin_page_mailpoet-form-editor {
|
||||
.BeaconFabButtonFrame,
|
||||
.BeaconContainer {
|
||||
left: 175px;
|
||||
}
|
||||
|
||||
&.folded {
|
||||
.BeaconFabButtonFrame,
|
||||
.BeaconContainer {
|
||||
left: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@include respond-to(medium-screen) {
|
||||
.BeaconFabButtonFrame,
|
||||
.BeaconContainer {
|
||||
left: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@include respond-to(small-screen) {
|
||||
.BeaconFabButtonFrame,
|
||||
.BeaconContainer {
|
||||
left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
@ -123,4 +123,5 @@
|
||||
// This style hides the horizontal scrollbar in Firefox browser
|
||||
.interface-interface-skeleton__sidebar {
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
64
mailpoet/assets/css/src/components-homepage/_upsell.scss
Normal file
64
mailpoet/assets/css/src/components-homepage/_upsell.scss
Normal file
@ -0,0 +1,64 @@
|
||||
.mailpoet-homepage-upsell {
|
||||
background: bottom right/290px no-repeat url('../../img/homepage/upsell-illustration.png') #fffaf2;
|
||||
min-height: 288px;
|
||||
|
||||
.mailpoet-homepage-section__heading {
|
||||
border: none;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
|
||||
h2 {
|
||||
background-color: $color-white;
|
||||
max-width: 330px;
|
||||
padding: 32px 20px 0 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-homepage-section__heading-after {
|
||||
bottom: 20px;
|
||||
position: relative;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.mailpoet-homepage-upsell__content {
|
||||
background-color: $color-white;
|
||||
max-width: 330px;
|
||||
padding: 8px 20px 32px 40px;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0 0 8px;
|
||||
|
||||
li {
|
||||
line-height: 16px;
|
||||
margin: 4px 0;
|
||||
|
||||
svg {
|
||||
fill: $color-secondary-middle;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
span {
|
||||
line-height: 18px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include respond-to(small-screen) {
|
||||
.mailpoet-homepage-upsell {
|
||||
background: $color-white;
|
||||
|
||||
.mailpoet-homepage-section__heading {
|
||||
h2 {
|
||||
max-width: 270px;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-homepage-upsell__content {
|
||||
max-width: 270px;
|
||||
}
|
||||
}
|
||||
}
|
@ -56,6 +56,7 @@ p.sender_email_address_warning:first-child {
|
||||
// Fix for select 2 placeholder padding rendering issue in Chrome
|
||||
.select2-container .select2-search--inline,
|
||||
.select2-container .select2-search--inline .select2-search__field {
|
||||
height: 22px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
@ -27,10 +27,6 @@ input.select2-search__field:-ms-input-placeholder {
|
||||
color: $color-placeholder-select2;
|
||||
}
|
||||
|
||||
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||
border: 1px solid #aaa; /* default Select2 border for single dropdown */
|
||||
}
|
||||
|
||||
textarea.regular-text {
|
||||
width: 25em !important;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
#mailpoet_landingpage_container {
|
||||
$content-padding: 32px 65px;
|
||||
$mobile-content-padding: 25px;
|
||||
$landingpage-max-width: 1460px;
|
||||
|
||||
.mailpoet-content-center {
|
||||
text-align: center;
|
||||
@ -130,6 +131,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
margin: 0 auto;
|
||||
max-width: $landingpage-max-width;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-faq-accordion {
|
||||
|
@ -4,6 +4,6 @@
|
||||
}
|
||||
|
||||
// Fix for 3rd party plugins icons in menu that might display broken because we block loading 3rd party CSS on mailepoet pages
|
||||
#adminmenu .wp-menu-image img {
|
||||
#adminmenu :not(.toplevel_page_site-card) .wp-menu-image img {
|
||||
max-width: 20px;
|
||||
}
|
||||
|
@ -71,6 +71,7 @@
|
||||
|
||||
.mailpoet-wizard-step-content {
|
||||
max-width: 480px;
|
||||
min-height: 300px;
|
||||
width: 100%;
|
||||
|
||||
@include respond-to(medium-screen) {
|
||||
@ -153,3 +154,19 @@
|
||||
.mailpoet-wizard-woocommerce-toggle {
|
||||
margin-left: $grid-gap;
|
||||
}
|
||||
|
||||
.mailpoet-welcome-wizard-confirmation-modal {
|
||||
width: 25%;
|
||||
|
||||
.mailpoet-welcome-wizard-confirmation-modal-buttons {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-welcome-wizard-mss-list ul {
|
||||
list-style-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 6 6'%3E%3Cpath fill='%237ED321' fill-rule='nonzero' d='M4.647.319c.246-.35.715-.423 1.048-.165.334.257.404.75.158 1.098L3.084 5.181c-.28.397-.835.429-1.154.066L.199 3.283c-.281-.319-.262-.816.042-1.11.305-.295.78-.275 1.06.044l1.115 1.266L4.646.319z' /%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
.key-activation-messages {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
@ -343,10 +343,12 @@ div.mailpoet_form_popup {
|
||||
.mailpoet_form_close_icon {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 20px;
|
||||
margin: 0 0 0 auto;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
width: 20px;
|
||||
z-index: 100002;
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,11 @@
|
||||
border: 0 !important;
|
||||
height: auto !important;
|
||||
outline: none;
|
||||
padding: 10px 5px 5px 16px !important;
|
||||
padding: 5px 5px 2px 16px !important;
|
||||
|
||||
&.select2-selection--multiple {
|
||||
padding: 7px 5px 0 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.select2-selection__arrow {
|
||||
@ -64,10 +68,6 @@
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.select2-selection--multiple .select2-selection__rendered {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.select2-selection__choice {
|
||||
background: $color-tertiary-light !important;
|
||||
border: 0 !important;
|
||||
@ -98,7 +98,7 @@
|
||||
|
||||
.select2-search--inline {
|
||||
display: inline-block;
|
||||
margin-bottom: 9px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.select2-search__field {
|
||||
|
File diff suppressed because one or more lines are too long
@ -34,7 +34,6 @@
|
||||
@import './components-form-editor/custom-field';
|
||||
@import './components-form-editor/form-title';
|
||||
@import './components-form-editor/header';
|
||||
@import './components-form-editor/helpscout';
|
||||
@import './components-form-editor/form-placement';
|
||||
@import './components-form-editor/preview';
|
||||
@import './components-form-editor/block-editor';
|
||||
|
@ -21,3 +21,4 @@
|
||||
@import 'components-homepage/_task-list';
|
||||
@import 'components-homepage/_content-section';
|
||||
@import 'components-homepage/_product-discovery';
|
||||
@import 'components-homepage/_upsell';
|
||||
|
@ -1,3 +1,3 @@
|
||||
// WordPress breakpoints
|
||||
$mailpoet-breakpoint-small: 782px;
|
||||
$mailpoet-breakpoint-small: 781px;
|
||||
$mailpoet-breakpoint-medium: 960px;
|
||||
|
BIN
mailpoet/assets/img/homepage/upsell-illustration.png
Normal file
BIN
mailpoet/assets/img/homepage/upsell-illustration.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
@ -18,9 +18,14 @@ import _ from 'underscore';
|
||||
*/
|
||||
var eventsCache = [];
|
||||
|
||||
const LOCALSTORAGE_KEY = 'mailpoet-track-events-cache';
|
||||
export const CacheEventOptionSaveInStorage = 'saveInStorage';
|
||||
|
||||
function track(name, data = [], options = {}, callback = null) {
|
||||
let trackedData = data;
|
||||
|
||||
const optionsData = options === CacheEventOptionSaveInStorage ? {} : options;
|
||||
|
||||
if (typeof window.mixpanel.track !== 'function') {
|
||||
window.mixpanel.init(window.mixpanelTrackingId);
|
||||
}
|
||||
@ -33,7 +38,7 @@ function track(name, data = [], options = {}, callback = null) {
|
||||
trackedData['MailPoet Premium version'] = window.mailpoet_premium_version;
|
||||
}
|
||||
|
||||
window.mixpanel.track(name, trackedData, options, callback);
|
||||
window.mixpanel.track(name, trackedData, optionsData, callback);
|
||||
}
|
||||
|
||||
function exportMixpanel() {
|
||||
@ -58,12 +63,22 @@ function exportMixpanel() {
|
||||
}
|
||||
}
|
||||
|
||||
function trackIfEnabled(event) {
|
||||
if (window.mailpoet_analytics_enabled || event.forced) {
|
||||
track(event.name, event.data, event.options);
|
||||
}
|
||||
}
|
||||
|
||||
function trackCachedEvents() {
|
||||
eventsCache.forEach(function trackIfEnabled(event) {
|
||||
if (window.mailpoet_analytics_enabled || event.forced) {
|
||||
track(event.name, event.data, event.options);
|
||||
}
|
||||
});
|
||||
const storageItem = localStorage.getItem(LOCALSTORAGE_KEY);
|
||||
if (storageItem && window.mailpoet_analytics_enabled) {
|
||||
const localEventsCache = JSON.parse(storageItem);
|
||||
localEventsCache.forEach(trackIfEnabled);
|
||||
localStorage.removeItem(LOCALSTORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
eventsCache.forEach(trackIfEnabled);
|
||||
}
|
||||
|
||||
function cacheEvent(forced, name, data, options, callback) {
|
||||
@ -73,12 +88,15 @@ function cacheEvent(forced, name, data, options, callback) {
|
||||
options: options,
|
||||
forced: forced,
|
||||
});
|
||||
if (options === CacheEventOptionSaveInStorage) {
|
||||
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(eventsCache));
|
||||
}
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
function initializeMixpanelWhenLoaded() {
|
||||
export function initializeMixpanelWhenLoaded() {
|
||||
if (typeof window.mixpanel === 'object') {
|
||||
exportMixpanel();
|
||||
trackCachedEvents();
|
||||
@ -89,5 +107,3 @@ function initializeMixpanelWhenLoaded() {
|
||||
|
||||
export const MailPoetTrackEvent = _.partial(cacheEvent, false);
|
||||
export const MailPoetForceTrackEvent = _.partial(cacheEvent, true);
|
||||
|
||||
initializeMixpanelWhenLoaded();
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import { locale } from '../../config';
|
||||
|
||||
type Item = {
|
||||
key: string;
|
||||
@ -15,7 +16,7 @@ export function Statistics({
|
||||
items,
|
||||
labelPosition = 'before',
|
||||
}: Props): JSX.Element {
|
||||
const intl = new Intl.NumberFormat();
|
||||
const intl = new Intl.NumberFormat(locale.toString());
|
||||
return (
|
||||
<div className="mailpoet-automation-stats">
|
||||
{items.map((item, i) => (
|
||||
|
@ -4,9 +4,29 @@ declare global {
|
||||
root: string;
|
||||
nonce: string;
|
||||
};
|
||||
mailpoet_locale_full: string;
|
||||
mailpoet_automation_count: number;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = window.mailpoet_automation_api;
|
||||
export const automationCount = window.mailpoet_automation_count;
|
||||
|
||||
// export locale to use with Intl APIs
|
||||
export const locale: Intl.Locale = (() => {
|
||||
const tag = (
|
||||
window.mailpoet_locale_full ??
|
||||
document.documentElement.lang ??
|
||||
'en'
|
||||
).replace('_', '-');
|
||||
|
||||
try {
|
||||
return new Intl.Locale(tag);
|
||||
} catch (_) {
|
||||
try {
|
||||
return new Intl.Locale(tag.split('-')[0]);
|
||||
} catch (__) {
|
||||
return new Intl.Locale('en');
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
@ -3,6 +3,7 @@ import { useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { storeName } from '../../../store';
|
||||
import { TrashButton } from '../../actions/trash-button';
|
||||
import { locale } from '../../../../config';
|
||||
|
||||
export function AutomationSidebar(): JSX.Element {
|
||||
const { automationData } = useSelect(
|
||||
@ -23,7 +24,7 @@ export function AutomationSidebar(): JSX.Element {
|
||||
<PanelRow>
|
||||
<strong>Date added</strong>{' '}
|
||||
{new Date(Date.parse(automationData.created_at)).toLocaleDateString(
|
||||
undefined,
|
||||
locale.toString(),
|
||||
dateOptions,
|
||||
)}
|
||||
</PanelRow>
|
||||
@ -31,13 +32,13 @@ export function AutomationSidebar(): JSX.Element {
|
||||
<strong>Activated</strong>{' '}
|
||||
{automationData.status === 'active' &&
|
||||
new Date(Date.parse(automationData.updated_at)).toLocaleDateString(
|
||||
undefined,
|
||||
locale.toString(),
|
||||
dateOptions,
|
||||
)}
|
||||
{automationData.status !== 'active' &&
|
||||
automationData.activated_at &&
|
||||
new Date(Date.parse(automationData.activated_at)).toLocaleDateString(
|
||||
undefined,
|
||||
locale.toString(),
|
||||
dateOptions,
|
||||
)}
|
||||
{automationData.status !== 'active' && !automationData.activated_at && (
|
||||
|
@ -3,6 +3,7 @@ import { AutomationEditorWindow, State } from './types';
|
||||
declare let window: AutomationEditorWindow;
|
||||
|
||||
export const getInitialState = (): State => ({
|
||||
registry: { ...window.mailpoet_automation_registry },
|
||||
context: { ...window.mailpoet_automation_context },
|
||||
stepTypes: {},
|
||||
automationData: { ...window.mailpoet_automation },
|
||||
|
@ -2,7 +2,14 @@ import { createRegistrySelector } from '@wordpress/data';
|
||||
import { store as interfaceStore } from '@wordpress/interface';
|
||||
import { store as preferencesStore } from '@wordpress/preferences';
|
||||
import { storeName } from './constants';
|
||||
import { Context, Errors, Feature, State, StepErrors, StepType } from './types';
|
||||
import {
|
||||
Registry,
|
||||
Errors,
|
||||
Feature,
|
||||
State,
|
||||
StepErrors,
|
||||
StepType,
|
||||
} from './types';
|
||||
import { Item } from '../components/inserter/item';
|
||||
import { Step, Automation } from '../components/automation/types';
|
||||
|
||||
@ -25,15 +32,22 @@ export function isActivationPanelOpened(state: State): boolean {
|
||||
return state.activationPanel.isOpened;
|
||||
}
|
||||
|
||||
export function getContext(state: State): Context {
|
||||
return state.context;
|
||||
export function getRegistry(state: State): Registry {
|
||||
return state.registry;
|
||||
}
|
||||
|
||||
export function getContextStep(
|
||||
export function getRegistryStep(
|
||||
state: State,
|
||||
key: string,
|
||||
): Context['steps'][number] | undefined {
|
||||
return state.context.steps[key];
|
||||
): Registry['steps'][number] | undefined {
|
||||
return state.registry.steps[key];
|
||||
}
|
||||
|
||||
export function getContext<T = unknown>(
|
||||
state: State,
|
||||
key: string,
|
||||
): T | undefined {
|
||||
return state.context[key] as T | undefined;
|
||||
}
|
||||
|
||||
export function getSteps(state: State): StepType[] {
|
||||
|
@ -2,11 +2,12 @@ import { ComponentType } from 'react';
|
||||
import { Step, Automation } from '../components/automation/types';
|
||||
|
||||
export interface AutomationEditorWindow extends Window {
|
||||
mailpoet_automation_registry: Registry;
|
||||
mailpoet_automation_context: Context;
|
||||
mailpoet_automation: Automation;
|
||||
}
|
||||
|
||||
export type Context = {
|
||||
export type Registry = {
|
||||
steps: Record<
|
||||
string,
|
||||
{
|
||||
@ -20,6 +21,8 @@ export type Context = {
|
||||
>;
|
||||
};
|
||||
|
||||
export type Context = Record<string, unknown>;
|
||||
|
||||
export type StepGroup = 'actions' | 'logical' | 'triggers';
|
||||
|
||||
export type StepType = {
|
||||
@ -46,6 +49,7 @@ export type Errors = {
|
||||
};
|
||||
|
||||
export type State = {
|
||||
registry: Registry;
|
||||
context: Context;
|
||||
stepTypes: Record<string, StepType>;
|
||||
automationData: Automation;
|
||||
|
@ -1,3 +1,4 @@
|
||||
// exports for extensibility
|
||||
export { id } from './id';
|
||||
export * as config from './config';
|
||||
export * as EditorStore from './editor/store';
|
||||
|
@ -0,0 +1,14 @@
|
||||
import { select } from '@wordpress/data';
|
||||
import { FormTokenItem } from '../../editor/components';
|
||||
import { storeName } from '../../editor/store';
|
||||
|
||||
type Segment = FormTokenItem & {
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type Context = {
|
||||
segments?: Segment[];
|
||||
};
|
||||
|
||||
export const getContext = (): Context =>
|
||||
select(storeName).getContext('mailpoet') as Context;
|
@ -6,6 +6,7 @@ import { step as AddTagsAction } from './steps/add_tags';
|
||||
import { step as RemoveTagsAction } from './steps/remove_tags';
|
||||
import { step as AddToListStep } from './steps/add_to_list';
|
||||
import { step as RemoveFromListStep } from './steps/remove_from_list';
|
||||
import { step as UpdateSubscriberStep } from './steps/update-subscriber';
|
||||
import { registerStepControls } from './step-controls';
|
||||
|
||||
export const initialize = (): void => {
|
||||
@ -16,5 +17,6 @@ export const initialize = (): void => {
|
||||
registerStepType(RemoveTagsAction);
|
||||
registerStepType(AddToListStep);
|
||||
registerStepType(RemoveFromListStep);
|
||||
registerStepType(UpdateSubscriberStep);
|
||||
registerStepControls();
|
||||
};
|
||||
|
@ -11,9 +11,9 @@ type ReplyToArgs = {
|
||||
};
|
||||
|
||||
export function ReplyToPanel(): JSX.Element {
|
||||
const { context, selectedStep, errors } = useSelect(
|
||||
const { registry, selectedStep, errors } = useSelect(
|
||||
(select) => ({
|
||||
context: select(storeName).getContext(),
|
||||
registry: select(storeName).getRegistry(),
|
||||
selectedStep: select(storeName).getSelectedStep(),
|
||||
errors: select(storeName).getStepError(
|
||||
select(storeName).getSelectedStep().id,
|
||||
@ -30,10 +30,10 @@ export function ReplyToPanel(): JSX.Element {
|
||||
const prevValue = useRef<{ name?: string; address?: string }>();
|
||||
|
||||
// defaults
|
||||
const argsContext =
|
||||
context.steps['mailpoet:send-email']?.args_schema?.properties ?? {};
|
||||
const defaultName = argsContext.reply_to_name?.default;
|
||||
const defaultAddress = argsContext.reply_to_address?.default;
|
||||
const argsSchema =
|
||||
registry.steps['mailpoet:send-email']?.args_schema?.properties ?? {};
|
||||
const defaultName = argsSchema.reply_to_name?.default;
|
||||
const defaultAddress = argsSchema.reply_to_address?.default;
|
||||
|
||||
const errorFields = errors?.fields ?? {};
|
||||
const replyToNameError = errorFields?.reply_to_name ?? '';
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { PanelBody } from '@wordpress/components';
|
||||
import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { getContext } from '../../../context';
|
||||
import { storeName } from '../../../../../editor/store';
|
||||
import { segments } from './segment';
|
||||
import {
|
||||
PlainBodyTitle,
|
||||
FormTokenField,
|
||||
@ -20,7 +20,7 @@ export function ListPanel(): JSX.Element {
|
||||
? (selectedStep.args.segment_ids as number[])
|
||||
: [];
|
||||
|
||||
const validSegments = segments.filter(
|
||||
const validSegments = getContext().segments.filter(
|
||||
(segment) => segment.type === 'default',
|
||||
);
|
||||
const selected = validSegments.filter((segment): boolean =>
|
||||
|
@ -1,13 +0,0 @@
|
||||
import { FormTokenItem } from '../../../../../editor/components';
|
||||
|
||||
type Segment = FormTokenItem & {
|
||||
type: string;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
mailpoet_segments: Segment[];
|
||||
}
|
||||
}
|
||||
|
||||
export const segments = window.mailpoet_segments;
|
@ -0,0 +1,33 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { postAuthor } from '@wordpress/icons';
|
||||
import { StepType } from '../../../../editor/store/types';
|
||||
import { PremiumModalForStepEdit } from '../../../../../common/premium_modal';
|
||||
import { LockedBadge } from '../../../../../common/premium_modal/locked_badge';
|
||||
|
||||
export const step: StepType = {
|
||||
key: 'mailpoet:update-subscriber',
|
||||
group: 'actions',
|
||||
title: __('Update subscriber', 'mailpoet'),
|
||||
description: __(
|
||||
'Update the subscriber’s custom field to a specific value.',
|
||||
'mailpoet',
|
||||
),
|
||||
subtitle: () => <LockedBadge text={__('Premium', 'mailpoet')} />,
|
||||
foreground: '#00A32A',
|
||||
background: '#EDFAEF',
|
||||
icon: () => (
|
||||
<div style={{ width: '100%', height: '100%', scale: '1.3' }}>
|
||||
{postAuthor}
|
||||
</div>
|
||||
),
|
||||
edit: () => (
|
||||
<PremiumModalForStepEdit
|
||||
tracking={{
|
||||
utm_medium: 'upsell_modal',
|
||||
utm_campaign: 'create_automation_editor_update_subscriber',
|
||||
}}
|
||||
>
|
||||
{__('Updating subscribers is a premium feature.', 'mailpoet')}
|
||||
</PremiumModalForStepEdit>
|
||||
),
|
||||
} as const;
|
@ -19,7 +19,7 @@ const makeApiRequest = (domain: string) =>
|
||||
*
|
||||
* returns `false` if not required, `true` if DMARC policy is Restricted
|
||||
* @param {string} email Email address
|
||||
* @param {boolean} isMssActive Is MailPoet sending service active?
|
||||
* @param {boolean} isMssActive Is MailPoet Sending Service active?
|
||||
* @returns {Promise<boolean>} false if not required, `true` if DMARC policy is Restricted
|
||||
*/
|
||||
const checkSenderEmailDomainDmarcPolicy = async (
|
||||
|
@ -1,17 +1,24 @@
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { isErrorResponse } from '../../ajax';
|
||||
|
||||
export async function callApi(actionData) {
|
||||
export async function callApi<D = unknown, M = unknown>(
|
||||
actionData,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
res: { data: D; meta?: M };
|
||||
error?: unknown;
|
||||
}> {
|
||||
const { endpoint, action, data } = actionData;
|
||||
try {
|
||||
const res = await MailPoet.Ajax.post({
|
||||
const res = await MailPoet.Ajax.post<{ data: D; meta?: M }>({
|
||||
api_version: MailPoet.apiVersion,
|
||||
endpoint,
|
||||
action,
|
||||
data,
|
||||
});
|
||||
return { success: true, res };
|
||||
} catch (res) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (res: any) {
|
||||
const error = isErrorResponse(res)
|
||||
? res.errors.map((e) => e.message)
|
||||
: null;
|
||||
|
2
mailpoet/assets/js/src/common/controls/index.ts
Normal file
2
mailpoet/assets/js/src/common/controls/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './call_api';
|
||||
export * from './track_event';
|
@ -1,6 +1,7 @@
|
||||
export { Button } from './button/button';
|
||||
export * from './loader/loader';
|
||||
export * from './tabs/tab';
|
||||
export * from './typography';
|
||||
export { Heading as TypographyHeading } from './typography/heading/heading';
|
||||
export * from './premium_required/premium_required';
|
||||
export * from './loading';
|
||||
@ -8,3 +9,7 @@ export * from './form';
|
||||
export * from './tag';
|
||||
export * from './listings';
|
||||
export * from './error_boundary';
|
||||
export * from './functions';
|
||||
export * from './utils';
|
||||
export * from './thumbnail';
|
||||
export * from './controls';
|
||||
|
@ -0,0 +1,96 @@
|
||||
import { Button } from 'common/index';
|
||||
import { t } from 'common/functions';
|
||||
import { Messages } from 'common/premium_key/messages';
|
||||
import { MssStatus } from 'settings/store/types';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { select } from '@wordpress/data';
|
||||
import { STORE_NAME } from 'settings/store/store_name';
|
||||
import { useContext, useState } from 'react';
|
||||
import { GlobalContext } from 'context';
|
||||
import { useAction, useSelector, useSetting } from 'settings/store/hooks';
|
||||
|
||||
type KeyState = {
|
||||
is_approved: boolean;
|
||||
};
|
||||
|
||||
type KeyActivationButtonPropType = {
|
||||
label: string;
|
||||
isFullWidth?: boolean;
|
||||
};
|
||||
|
||||
export function KeyActivationButton({
|
||||
label,
|
||||
isFullWidth = false,
|
||||
}: KeyActivationButtonPropType) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { notices } = useContext<any>(GlobalContext);
|
||||
const state = useSelector('getKeyActivationState')();
|
||||
const setState = useAction('updateKeyActivationState');
|
||||
const verifyMssKey = useAction('verifyMssKey');
|
||||
const verifyPremiumKey = useAction('verifyPremiumKey');
|
||||
const sendCongratulatoryMssEmail = useAction('sendCongratulatoryMssEmail');
|
||||
const [apiKeyState] = useSetting('mta', 'mailpoet_api_key_state', 'data');
|
||||
|
||||
async function activationCallback() {
|
||||
await verifyMssKey(state.key);
|
||||
sendCongratulatoryMssEmail();
|
||||
setState({ fromAddressModalCanBeShown: true });
|
||||
}
|
||||
|
||||
const showPendingApprovalNotice =
|
||||
state.inProgress === false &&
|
||||
state.mssStatus === MssStatus.VALID_MSS_ACTIVE &&
|
||||
apiKeyState &&
|
||||
(apiKeyState as KeyState).is_approved === false;
|
||||
|
||||
const buttonIsDisabled = state.key === '' || state.key === null;
|
||||
const [showRefreshMessage, setShowRefreshMessage] = useState(true);
|
||||
|
||||
const verifyKey = async () => {
|
||||
if (!state.key) {
|
||||
notices.error(<p>{t('premiumTabNoKeyNotice')}</p>, { scroll: true });
|
||||
return;
|
||||
}
|
||||
await setState({
|
||||
mssStatus: null,
|
||||
premiumStatus: null,
|
||||
premiumInstallationStatus: null,
|
||||
});
|
||||
MailPoet.Modal.loading(true);
|
||||
setState({ inProgress: true });
|
||||
await verifyMssKey(state.key);
|
||||
const currentMssStatus =
|
||||
select(STORE_NAME).getKeyActivationState().mssStatus;
|
||||
if (currentMssStatus === MssStatus.VALID_MSS_ACTIVE) {
|
||||
await sendCongratulatoryMssEmail();
|
||||
}
|
||||
await verifyPremiumKey(state.key);
|
||||
setState({ inProgress: false });
|
||||
MailPoet.Modal.loading(false);
|
||||
setState({ fromAddressModalCanBeShown: true });
|
||||
// pending approval refresh link should only show on refresh of the page and should get hidden after the refresh button is clicked
|
||||
setShowRefreshMessage(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className="mailpoet-verify-key-button"
|
||||
type="button"
|
||||
onClick={verifyKey}
|
||||
isFullWidth={isFullWidth}
|
||||
isDisabled={buttonIsDisabled}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
{state.isKeyValid !== null &&
|
||||
Messages(
|
||||
state,
|
||||
showPendingApprovalNotice,
|
||||
activationCallback,
|
||||
verifyKey,
|
||||
showRefreshMessage,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
34
mailpoet/assets/js/src/common/premium_key/key_input.tsx
Normal file
34
mailpoet/assets/js/src/common/premium_key/key_input.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Input } from 'common/index';
|
||||
import { useAction, useSelector } from 'settings/store/hooks';
|
||||
|
||||
type KeyInputPropType = {
|
||||
placeholder?: string;
|
||||
isFullWidth?: boolean;
|
||||
};
|
||||
|
||||
export function KeyInput({
|
||||
placeholder,
|
||||
isFullWidth = false,
|
||||
}: KeyInputPropType) {
|
||||
const state = useSelector('getKeyActivationState')();
|
||||
const setState = useAction('updateKeyActivationState');
|
||||
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
id="mailpoet_premium_key"
|
||||
name="premium[premium_key]"
|
||||
placeholder={placeholder}
|
||||
isFullWidth={isFullWidth}
|
||||
value={state.key || ''}
|
||||
onChange={(event) =>
|
||||
setState({
|
||||
mssStatus: null,
|
||||
premiumStatus: null,
|
||||
premiumInstallationStatus: null,
|
||||
key: event.target.value.trim() || null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { useSelector } from 'settings/store/hooks/index';
|
||||
import { useSelector } from 'settings/store/hooks';
|
||||
import { PremiumStatus } from 'settings/store/types';
|
||||
import { Button } from 'common/button/button';
|
||||
|
89
mailpoet/assets/js/src/common/premium_key/messages.tsx
Normal file
89
mailpoet/assets/js/src/common/premium_key/messages.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import ReactStringReplace from 'react-string-replace';
|
||||
import { KeyActivationState } from 'settings/store/types';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import {
|
||||
KeyMessages,
|
||||
MssMessages,
|
||||
PremiumMessages,
|
||||
ServiceUnavailableMessage,
|
||||
} from './key_messages';
|
||||
import { getLinkRegex } from '../utils';
|
||||
|
||||
export function Messages(
|
||||
state: KeyActivationState,
|
||||
showPendingApprovalNotice: boolean,
|
||||
activationCallback: () => Promise<void>,
|
||||
verifyKey: () => Promise<void>,
|
||||
showRefreshMessage: boolean,
|
||||
) {
|
||||
if (state.code === 503) {
|
||||
return (
|
||||
<div className="key-activation-messages">
|
||||
<ServiceUnavailableMessage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const onRefreshClick = async (e: MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
await verifyKey();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="key-activation-messages">
|
||||
<KeyMessages />
|
||||
{state.mssStatus !== null && (
|
||||
<MssMessages
|
||||
keyMessage={state.mssMessage}
|
||||
activationCallback={activationCallback}
|
||||
/>
|
||||
)}
|
||||
{state.congratulatoryMssEmailSentTo && (
|
||||
<div className="mailpoet_success_item mailpoet_success">
|
||||
{MailPoet.I18n.t('premiumTabCongratulatoryMssEmailSent').replace(
|
||||
'[email_address]',
|
||||
state.congratulatoryMssEmailSentTo,
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{state.premiumStatus !== null && (
|
||||
<PremiumMessages keyMessage={state.premiumMessage} />
|
||||
)}
|
||||
|
||||
{showPendingApprovalNotice && (
|
||||
<div className="mailpoet_success">
|
||||
<div className="pending_approval_heading">
|
||||
{MailPoet.I18n.t('premiumTabPendingApprovalHeading')}
|
||||
</div>
|
||||
<div>
|
||||
{MailPoet.I18n.t('premiumTabPendingApprovalMessage')}{' '}
|
||||
{showRefreshMessage &&
|
||||
ReactStringReplace(
|
||||
MailPoet.I18n.t('premiumTabPendingApprovalMessageRefresh'),
|
||||
getLinkRegex(),
|
||||
(match) => (
|
||||
<a onClick={onRefreshClick} href="#">
|
||||
{match}
|
||||
</a>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!state.isKeyValid && (
|
||||
<p>
|
||||
<a
|
||||
href="https://kb.mailpoet.com/article/319-known-errors-when-validating-a-mailpoet-key"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-beacon-article="5ef1da9d2c7d3a10cba966c5"
|
||||
className="mailpoet_error"
|
||||
>
|
||||
{MailPoet.I18n.t('learnMore')}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
2
mailpoet/assets/js/src/common/typography/index.ts
Normal file
2
mailpoet/assets/js/src/common/typography/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './heading/heading';
|
||||
export * from './list/list';
|
3
mailpoet/assets/js/src/common/utils.ts
Normal file
3
mailpoet/assets/js/src/common/utils.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const getLinkRegex = () => /\[link\](.*?)\[\/link\]/g;
|
||||
export const isTruthy = (value: string | number | boolean) =>
|
||||
[1, '1', true, 'true'].includes(value);
|
@ -2,7 +2,11 @@ import { select, dispatch } from '@wordpress/data';
|
||||
import { SETTINGS_DEFAULTS } from '@wordpress/block-editor';
|
||||
import { blocksToFormBodyFactory } from './blocks_to_form_body';
|
||||
import { mapFormDataBeforeSaving } from './map_form_data_before_saving';
|
||||
import { ToggleAction, ToggleBlockInserterAction } from './actions_types';
|
||||
import {
|
||||
CustomFieldStartedAction,
|
||||
ToggleAction,
|
||||
ToggleBlockInserterAction,
|
||||
} from './actions_types';
|
||||
import { BlockInsertionPoint } from './state_types';
|
||||
|
||||
export function toggleSidebar(toggleTo): ToggleAction {
|
||||
@ -119,7 +123,9 @@ export function createCustomFieldDone(response) {
|
||||
};
|
||||
}
|
||||
|
||||
export function createCustomFieldStarted(customField) {
|
||||
export function createCustomFieldStarted(
|
||||
customField,
|
||||
): CustomFieldStartedAction {
|
||||
return {
|
||||
type: 'CREATE_CUSTOM_FIELD_STARTED',
|
||||
customField,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { BlockInsertionPoint } from './state_types';
|
||||
import { CustomField } from './form_data_types';
|
||||
|
||||
export type ToggleAction = {
|
||||
type: string;
|
||||
@ -9,3 +10,14 @@ export type ToggleBlockInserterAction = {
|
||||
type: string;
|
||||
value: boolean | BlockInsertionPoint;
|
||||
};
|
||||
|
||||
export type CustomFieldStartedAction = {
|
||||
type: 'CREATE_CUSTOM_FIELD_STARTED';
|
||||
customField: CustomField;
|
||||
};
|
||||
|
||||
export type ToggleSidebarPanelAction = {
|
||||
type: 'TOGGLE_SIDEBAR_PANEL';
|
||||
id: string;
|
||||
toggleTo?: boolean;
|
||||
};
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { has } from 'lodash';
|
||||
import { BlockInstance } from '@wordpress/blocks';
|
||||
import {
|
||||
FontSizeDefinition,
|
||||
ColorDefinition,
|
||||
GradientDefinition,
|
||||
CustomField,
|
||||
InputBlockStyles,
|
||||
} from 'form_editor/store/form_data_types';
|
||||
import {
|
||||
mapInputBlockStyles,
|
||||
mapColorSlugToValue,
|
||||
@ -6,7 +14,11 @@ import {
|
||||
mapGradientSlugToValue,
|
||||
} from './mapping/from_blocks/styles_mapper';
|
||||
|
||||
const mapCustomField = (block, customFields, mappedCommonProperties) => {
|
||||
const mapCustomField = (
|
||||
block: BlockInstance,
|
||||
customFields: CustomField[],
|
||||
mappedCommonProperties,
|
||||
) => {
|
||||
const customField = customFields.find(
|
||||
(cf) => cf.id === block.attributes.customFieldId,
|
||||
);
|
||||
@ -21,11 +33,15 @@ const mapCustomField = (block, customFields, mappedCommonProperties) => {
|
||||
}
|
||||
if (block.name.startsWith('mailpoet-form/custom-text')) {
|
||||
mapped.type = 'text';
|
||||
mapped.styles = mapInputBlockStyles(block.attributes.styles);
|
||||
mapped.styles = mapInputBlockStyles(
|
||||
block.attributes.styles as unknown as InputBlockStyles,
|
||||
);
|
||||
}
|
||||
if (block.name.startsWith('mailpoet-form/custom-textarea')) {
|
||||
mapped.type = 'textarea';
|
||||
mapped.styles = mapInputBlockStyles(block.attributes.styles);
|
||||
mapped.styles = mapInputBlockStyles(
|
||||
block.attributes.styles as unknown as InputBlockStyles,
|
||||
);
|
||||
}
|
||||
if (block.name.startsWith('mailpoet-form/custom-radio')) {
|
||||
mapped.type = 'radio';
|
||||
@ -56,7 +72,7 @@ const mapCustomField = (block, customFields, mappedCommonProperties) => {
|
||||
}
|
||||
if (has(block.attributes, 'values')) {
|
||||
mapped.params.values = block.attributes.values.map((value) => {
|
||||
const mappedValue = {
|
||||
const mappedValue: Record<string, unknown> = {
|
||||
value: value.name,
|
||||
};
|
||||
if (has(value, 'isChecked') && value.isChecked) {
|
||||
@ -76,10 +92,10 @@ const mapCustomField = (block, customFields, mappedCommonProperties) => {
|
||||
* @param customFields - list of all custom Fields
|
||||
*/
|
||||
export const blocksToFormBodyFactory = (
|
||||
fontSizeDefinitions,
|
||||
colorDefinitions,
|
||||
gradientDefinitions,
|
||||
customFields,
|
||||
fontSizeDefinitions: FontSizeDefinition[],
|
||||
colorDefinitions: ColorDefinition[],
|
||||
gradientDefinitions: GradientDefinition[],
|
||||
customFields: CustomField[],
|
||||
) => {
|
||||
if (!Array.isArray(customFields)) {
|
||||
throw new Error('Mapper expects customFields to be an array.');
|
||||
@ -89,7 +105,7 @@ export const blocksToFormBodyFactory = (
|
||||
* @param blocks
|
||||
* @returns {*}
|
||||
*/
|
||||
const mapBlocks = (blocks) => {
|
||||
const mapBlocks = (blocks: BlockInstance[]) => {
|
||||
if (!Array.isArray(blocks)) {
|
||||
throw new Error('Mapper expects blocks to be an array.');
|
||||
}
|
||||
@ -101,7 +117,7 @@ export const blocksToFormBodyFactory = (
|
||||
params: {
|
||||
label: block.attributes.label,
|
||||
class_name: block.attributes.className || null,
|
||||
},
|
||||
} as Record<string, unknown>,
|
||||
};
|
||||
if (block.attributes.mandatory) {
|
||||
mapped.params.required = '1';
|
||||
@ -120,19 +136,22 @@ export const blocksToFormBodyFactory = (
|
||||
align: block.attributes.textAlign || 'left',
|
||||
font_size: mapFontSizeSlugToValue(
|
||||
fontSizeDefinitions,
|
||||
block.attributes.fontSize,
|
||||
block.attributes.style?.typography?.fontSize,
|
||||
block.attributes.fontSize as unknown as string,
|
||||
(block.attributes.style?.typography
|
||||
?.fontSize as unknown as number) || null,
|
||||
),
|
||||
text_color: mapColorSlugToValue(
|
||||
colorDefinitions,
|
||||
block.attributes.textColor,
|
||||
block.attributes.style?.color?.text,
|
||||
block.attributes.textColor as unknown as string,
|
||||
(block.attributes.style?.color?.text as unknown as string) ||
|
||||
null,
|
||||
),
|
||||
line_height: block.attributes.style?.typography?.lineHeight,
|
||||
background_color: mapColorSlugToValue(
|
||||
colorDefinitions,
|
||||
block.attributes.backgroundColor,
|
||||
block.attributes.style?.color?.background,
|
||||
block.attributes.backgroundColor as unknown as string,
|
||||
(block.attributes.style?.color
|
||||
?.background as unknown as string) || null,
|
||||
),
|
||||
anchor: block.attributes.anchor || null,
|
||||
class_name: block.attributes.className || null,
|
||||
@ -148,19 +167,22 @@ export const blocksToFormBodyFactory = (
|
||||
align: block.attributes.align || 'left',
|
||||
font_size: mapFontSizeSlugToValue(
|
||||
fontSizeDefinitions,
|
||||
block.attributes.fontSize,
|
||||
block.attributes.style?.typography?.fontSize,
|
||||
block.attributes.fontSize as unknown as string,
|
||||
(block.attributes.style?.typography
|
||||
?.fontSize as unknown as number) || null,
|
||||
),
|
||||
line_height: block.attributes.style?.typography?.lineHeight,
|
||||
text_color: mapColorSlugToValue(
|
||||
colorDefinitions,
|
||||
block.attributes.textColor,
|
||||
block.attributes.style?.color?.text,
|
||||
block.attributes.textColor as unknown as string,
|
||||
(block.attributes.style?.color?.text as unknown as string) ||
|
||||
null,
|
||||
),
|
||||
background_color: mapColorSlugToValue(
|
||||
colorDefinitions,
|
||||
block.attributes.backgroundColor,
|
||||
block.attributes.style?.color?.background,
|
||||
block.attributes.backgroundColor as unknown as string,
|
||||
(block.attributes.style?.color
|
||||
?.background as unknown as string) || null,
|
||||
),
|
||||
class_name: block.attributes.className || null,
|
||||
},
|
||||
@ -199,18 +221,21 @@ export const blocksToFormBodyFactory = (
|
||||
padding: block.attributes.style?.spacing?.padding || null,
|
||||
text_color: mapColorSlugToValue(
|
||||
colorDefinitions,
|
||||
block.attributes.textColor,
|
||||
block.attributes.style?.color?.text,
|
||||
block.attributes.textColor as unknown as string,
|
||||
(block.attributes.style?.color?.text as unknown as string) ||
|
||||
null,
|
||||
),
|
||||
background_color: mapColorSlugToValue(
|
||||
colorDefinitions,
|
||||
block.attributes.backgroundColor,
|
||||
block.attributes.style?.color?.background,
|
||||
block.attributes.backgroundColor as unknown as string,
|
||||
(block.attributes.style?.color
|
||||
?.background as unknown as string) || null,
|
||||
),
|
||||
gradient: mapGradientSlugToValue(
|
||||
gradientDefinitions,
|
||||
block.attributes.gradient,
|
||||
block.attributes.style?.color?.gradient,
|
||||
block.attributes.gradient as unknown as string,
|
||||
(block.attributes.style?.color
|
||||
?.gradient as unknown as string) || null,
|
||||
),
|
||||
},
|
||||
};
|
||||
@ -229,18 +254,21 @@ export const blocksToFormBodyFactory = (
|
||||
padding: block.attributes.style?.spacing?.padding || null,
|
||||
text_color: mapColorSlugToValue(
|
||||
colorDefinitions,
|
||||
block.attributes.textColor,
|
||||
block.attributes.style?.color?.text,
|
||||
block.attributes.textColor as unknown as string,
|
||||
(block.attributes.style?.color?.text as unknown as string) ||
|
||||
null,
|
||||
),
|
||||
background_color: mapColorSlugToValue(
|
||||
colorDefinitions,
|
||||
block.attributes.backgroundColor,
|
||||
block.attributes.style?.color?.background,
|
||||
block.attributes.backgroundColor as unknown as string,
|
||||
(block.attributes.style?.color
|
||||
?.background as unknown as string) || null,
|
||||
),
|
||||
gradient: mapGradientSlugToValue(
|
||||
gradientDefinitions,
|
||||
block.attributes.gradient,
|
||||
block.attributes.style?.color?.gradient,
|
||||
block.attributes.gradient as unknown as string,
|
||||
(block.attributes.style?.color
|
||||
?.gradient as unknown as string) || null,
|
||||
),
|
||||
},
|
||||
};
|
||||
@ -253,21 +281,27 @@ export const blocksToFormBodyFactory = (
|
||||
...mapped.params,
|
||||
required: '1',
|
||||
},
|
||||
styles: mapInputBlockStyles(block.attributes.styles),
|
||||
styles: mapInputBlockStyles(
|
||||
block.attributes.styles as unknown as InputBlockStyles,
|
||||
),
|
||||
};
|
||||
case 'mailpoet-form/first-name-input':
|
||||
return {
|
||||
...mapped,
|
||||
id: 'first_name',
|
||||
name: 'First name',
|
||||
styles: mapInputBlockStyles(block.attributes.styles),
|
||||
styles: mapInputBlockStyles(
|
||||
block.attributes.styles as unknown as InputBlockStyles,
|
||||
),
|
||||
};
|
||||
case 'mailpoet-form/last-name-input':
|
||||
return {
|
||||
...mapped,
|
||||
id: 'last_name',
|
||||
name: 'Last name',
|
||||
styles: mapInputBlockStyles(block.attributes.styles),
|
||||
styles: mapInputBlockStyles(
|
||||
block.attributes.styles as unknown as InputBlockStyles,
|
||||
),
|
||||
};
|
||||
case 'mailpoet-form/segment-select':
|
||||
return {
|
||||
@ -289,7 +323,9 @@ export const blocksToFormBodyFactory = (
|
||||
id: 'submit',
|
||||
type: 'submit',
|
||||
name: 'Submit',
|
||||
styles: mapInputBlockStyles(block.attributes.styles),
|
||||
styles: mapInputBlockStyles(
|
||||
block.attributes.styles as unknown as InputBlockStyles,
|
||||
),
|
||||
};
|
||||
case 'mailpoet-form/divider':
|
||||
return {
|
@ -10,7 +10,7 @@ import {
|
||||
} from '@wordpress/blocks';
|
||||
import { callApi as CALL_API } from 'common/controls/call_api';
|
||||
import { SETTINGS_DEFAULTS } from '@wordpress/block-editor';
|
||||
import { blocksToFormBodyFactory } from './blocks_to_form_body.jsx';
|
||||
import { blocksToFormBodyFactory } from './blocks_to_form_body';
|
||||
import { registerCustomFieldBlock } from '../blocks/blocks.jsx';
|
||||
import { mapFormDataBeforeSaving } from './map_form_data_before_saving.jsx';
|
||||
import { findBlock } from './find_block';
|
||||
|
@ -79,6 +79,22 @@ export type FormSettingsType = {
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
export type FormData = {
|
||||
id: number | null;
|
||||
name: string;
|
||||
body: unknown[] | null;
|
||||
settings: FormSettingsType | null;
|
||||
styles: string | null;
|
||||
status: 'enabled' | 'disabled';
|
||||
created_at: { date: string; timezone_type: number; timezone: string };
|
||||
updated_at: { date: string; timezone_type: number; timezone: string };
|
||||
deleted_at: {
|
||||
date: string;
|
||||
timezone_type: number;
|
||||
timezone: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type InputBlockStyles = {
|
||||
fullWidth: boolean;
|
||||
inheritFromTheme: boolean;
|
||||
@ -108,6 +124,15 @@ export type InputBlockStylesServerData = {
|
||||
font_family?: string;
|
||||
};
|
||||
|
||||
export type CustomField = {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
params: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type ColorDefinition = {
|
||||
name: string;
|
||||
slug: string;
|
||||
|
@ -2,7 +2,7 @@ import { MailPoet } from 'mailpoet';
|
||||
import { createCustomFieldDone } from './reducers/create_custom_field_done.jsx';
|
||||
import { createCustomFieldFailed } from './reducers/create_custom_field_failed.jsx';
|
||||
import { customFieldEdited } from './reducers/custom_field_edited.jsx';
|
||||
import { createCustomFieldStartedFactory } from './reducers/create_custom_field_started.jsx';
|
||||
import { createCustomFieldStartedFactory } from './reducers/create_custom_field_started.ts';
|
||||
import { changeFormName } from './reducers/change_form_name.jsx';
|
||||
import { changeFormSettings } from './reducers/change_form_settings.jsx';
|
||||
import { changeFormStyles } from './reducers/change_form_styles.jsx';
|
||||
@ -17,13 +17,13 @@ import {
|
||||
} from './reducers/preview.jsx';
|
||||
import { saveFormDone } from './reducers/save_form_done.jsx';
|
||||
import { saveFormFailed } from './reducers/save_form_failed.jsx';
|
||||
import { saveFormStartedFactory } from './reducers/save_form_started.jsx';
|
||||
import { saveFormStartedFactory } from './reducers/save_form_started';
|
||||
import { switchDefaultSidebarTab } from './reducers/switch_sidebar_tab.jsx';
|
||||
import {
|
||||
toggleInserterSidebar,
|
||||
toggleSidebar,
|
||||
} from './reducers/toggle_sidebar.ts';
|
||||
import { toggleSidebarPanel } from './reducers/toggle_sidebar_panel.jsx';
|
||||
import { toggleSidebarPanel } from './reducers/toggle_sidebar_panel.ts';
|
||||
import { changeFormBlocks } from './reducers/change_form_blocks.jsx';
|
||||
import { saveCustomFieldDone } from './reducers/save_custom_field_done.jsx';
|
||||
import { saveCustomFieldFailed } from './reducers/save_custom_field_failed.jsx';
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { trim } from 'lodash';
|
||||
import { State } from '../state_types';
|
||||
import { CustomFieldStartedAction } from '../actions_types';
|
||||
|
||||
export const createCustomFieldStartedFactory =
|
||||
(MailPoet) => (state, action) => {
|
||||
(MailPoet) =>
|
||||
(state: State, action: CustomFieldStartedAction): State => {
|
||||
const notices = state.notices.filter(
|
||||
(notice) => notice.id !== 'custom-field',
|
||||
);
|
@ -1,15 +1,13 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import { HistoryRecord, State } from '../state_types';
|
||||
|
||||
const HISTORY_LENGTH = 100;
|
||||
const HISTORY_DEBOUNCE = 1000; // 1 second
|
||||
|
||||
type HistoryRecord = {
|
||||
blocks: unknown[];
|
||||
data: unknown[];
|
||||
time: number;
|
||||
};
|
||||
|
||||
const createRecord = (editorHistory: HistoryRecord[], state): HistoryRecord => {
|
||||
const createRecord = (
|
||||
editorHistory: HistoryRecord[],
|
||||
state: State,
|
||||
): HistoryRecord => {
|
||||
const lastHistoryRecord = editorHistory[editorHistory.length - 1];
|
||||
const time = Date.now();
|
||||
|
||||
@ -47,7 +45,7 @@ const createRecord = (editorHistory: HistoryRecord[], state): HistoryRecord => {
|
||||
return newHistoryRecord;
|
||||
};
|
||||
|
||||
export const createHistoryRecord = (state) => {
|
||||
export const createHistoryRecord = (state: State): State => {
|
||||
let editorHistory: HistoryRecord[] = state.editorHistory;
|
||||
let editorHistoryOffset: number = state.editorHistoryOffset;
|
||||
|
||||
@ -59,8 +57,7 @@ export const createHistoryRecord = (state) => {
|
||||
// When we want to create a history record, and we aren't at the end,
|
||||
// then we have to drop the rest of the history stack
|
||||
if (state.editorHistoryOffset !== 0) {
|
||||
const offset =
|
||||
state.editorHistory.length - ((state.editorHistoryOffset as number) + 1);
|
||||
const offset = state.editorHistory.length - (state.editorHistoryOffset + 1);
|
||||
editorHistoryOffset = 0;
|
||||
editorHistory = editorHistory.slice(0, offset);
|
||||
}
|
||||
@ -78,7 +75,7 @@ export const createHistoryRecord = (state) => {
|
||||
};
|
||||
};
|
||||
|
||||
const historyMove = (state, increment: number) => {
|
||||
const historyMove = (state: State, increment: number): State => {
|
||||
let offset: number = state.editorHistoryOffset;
|
||||
|
||||
// When we move undo, then we need save current state as last record in history
|
||||
@ -107,6 +104,6 @@ const historyMove = (state, increment: number) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const historyUndo = (state) => historyMove(state, 1);
|
||||
export const historyUndo = (state: State): State => historyMove(state, 1);
|
||||
|
||||
export const historyRedo = (state) => historyMove(state, -1);
|
||||
export const historyRedo = (state: State): State => historyMove(state, -1);
|
||||
|
@ -1,49 +0,0 @@
|
||||
export const saveFormStartedFactory = (MailPoet) => (state) => {
|
||||
// remove all form saving related notices
|
||||
const notices = state.notices.filter(
|
||||
(notice) =>
|
||||
![
|
||||
'missing-lists-in-custom-segments-block',
|
||||
'save-form',
|
||||
'missing-lists',
|
||||
'missing-block',
|
||||
].includes(notice.id),
|
||||
);
|
||||
const hasMissingLists =
|
||||
state.formErrors.includes('missing-lists') ||
|
||||
state.formErrors.includes('missing-lists-in-custom-segments-block');
|
||||
const sidebarOpenedPanels = [...state.sidebar.openedPanels];
|
||||
if (hasMissingLists) {
|
||||
notices.push({
|
||||
id: 'missing-lists',
|
||||
content: MailPoet.I18n.t('settingsPleaseSelectList'),
|
||||
isDismissible: true,
|
||||
status: 'error',
|
||||
});
|
||||
if (!sidebarOpenedPanels.includes('basic-settings')) {
|
||||
sidebarOpenedPanels.push('basic-settings');
|
||||
}
|
||||
}
|
||||
|
||||
const hasMissingEmail = state.formErrors.includes('missing-email-input');
|
||||
const hasMissingSubmit = state.formErrors.includes('missing-submit');
|
||||
if (hasMissingEmail || hasMissingSubmit) {
|
||||
notices.push({
|
||||
id: 'missing-block',
|
||||
content: MailPoet.I18n.t('missingObligatoryBlock'),
|
||||
isDismissible: true,
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
isFormSaving: !hasMissingLists,
|
||||
sidebar: {
|
||||
...state.sidebar,
|
||||
activeTab: hasMissingLists ? 'form' : state.sidebar.activeTab,
|
||||
openedPanels: sidebarOpenedPanels,
|
||||
},
|
||||
notices,
|
||||
};
|
||||
};
|
@ -0,0 +1,53 @@
|
||||
import { State } from '../state_types';
|
||||
|
||||
export const saveFormStartedFactory =
|
||||
(MailPoet) =>
|
||||
(state: State): State => {
|
||||
// remove all form saving related notices
|
||||
const notices = state.notices.filter(
|
||||
(notice) =>
|
||||
![
|
||||
'missing-lists-in-custom-segments-block',
|
||||
'save-form',
|
||||
'missing-lists',
|
||||
'missing-block',
|
||||
].includes(notice.id),
|
||||
);
|
||||
const hasMissingLists =
|
||||
state.formErrors.includes('missing-lists') ||
|
||||
state.formErrors.includes('missing-lists-in-custom-segments-block');
|
||||
const sidebarOpenedPanels = [...state.sidebar.openedPanels];
|
||||
if (hasMissingLists) {
|
||||
notices.push({
|
||||
id: 'missing-lists',
|
||||
content: MailPoet.I18n.t('settingsPleaseSelectList'),
|
||||
isDismissible: true,
|
||||
status: 'error',
|
||||
});
|
||||
if (!sidebarOpenedPanels.includes('basic-settings')) {
|
||||
sidebarOpenedPanels.push('basic-settings');
|
||||
}
|
||||
}
|
||||
|
||||
const hasMissingEmail = state.formErrors.includes('missing-email-input');
|
||||
const hasMissingSubmit = state.formErrors.includes('missing-submit');
|
||||
if (hasMissingEmail || hasMissingSubmit) {
|
||||
notices.push({
|
||||
id: 'missing-block',
|
||||
content: MailPoet.I18n.t('missingObligatoryBlock'),
|
||||
isDismissible: true,
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
isFormSaving: !hasMissingLists,
|
||||
sidebar: {
|
||||
...state.sidebar,
|
||||
activeTab: hasMissingLists ? 'form' : state.sidebar.activeTab,
|
||||
openedPanels: sidebarOpenedPanels,
|
||||
},
|
||||
notices,
|
||||
};
|
||||
};
|
@ -1,4 +1,6 @@
|
||||
import { remove } from 'lodash';
|
||||
import { ToggleSidebarPanelAction } from '../actions_types';
|
||||
import { State } from '../state_types';
|
||||
|
||||
const getRequiredAction = (openedPanels, panelId, toggleTo) => {
|
||||
const isPanelOpened = openedPanels.includes(panelId);
|
||||
@ -20,7 +22,10 @@ const getRequiredAction = (openedPanels, panelId, toggleTo) => {
|
||||
* @param {{toggleTo: string|undefined, id: string, type: string}} action
|
||||
* @return {object} Modified state object
|
||||
*/
|
||||
export const toggleSidebarPanel = (state, action) => {
|
||||
export const toggleSidebarPanel = (
|
||||
state: State,
|
||||
action: ToggleSidebarPanelAction,
|
||||
): State => {
|
||||
let toggleTo;
|
||||
if (action.toggleTo === true) toggleTo = 'opened';
|
||||
if (action.toggleTo === false) toggleTo = 'closed';
|
@ -1,35 +1,20 @@
|
||||
import { BlockInstance } from '@wordpress/blocks';
|
||||
import { FormSettingsType } from './form_data_types';
|
||||
import { FormData, CustomField } from './form_data_types';
|
||||
|
||||
export type BlockInsertionPoint = {
|
||||
rootClientId: string | undefined;
|
||||
insertionIndex: number | undefined;
|
||||
};
|
||||
|
||||
export type HistoryRecord = {
|
||||
blocks: BlockInstance[];
|
||||
data: FormData;
|
||||
time: number;
|
||||
};
|
||||
|
||||
export interface FormEditorWindow extends Window {
|
||||
mailpoet_custom_fields: {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
params: unknown;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}[];
|
||||
mailpoet_form_data: {
|
||||
id: number | null;
|
||||
name: string;
|
||||
body: unknown[] | null;
|
||||
settings: FormSettingsType | null;
|
||||
styles: string | null;
|
||||
status: 'enabled' | 'disabled';
|
||||
created_at: { date: string; timezone_type: number; timezone: string };
|
||||
updated_at: { date: string; timezone_type: number; timezone: string };
|
||||
deleted_at: {
|
||||
date: string;
|
||||
timezone_type: number;
|
||||
timezone: string;
|
||||
} | null;
|
||||
};
|
||||
mailpoet_custom_fields: CustomField[];
|
||||
mailpoet_form_data: FormData;
|
||||
mailpoet_date_types: {
|
||||
label: string;
|
||||
value: string;
|
||||
@ -71,7 +56,7 @@ export interface FormEditorWindow extends Window {
|
||||
declare let window: FormEditorWindow;
|
||||
|
||||
export type State = {
|
||||
editorHistory: unknown[];
|
||||
editorHistory: HistoryRecord[];
|
||||
editorHistoryOffset: number;
|
||||
formBlocks: BlockInstance[];
|
||||
formData: typeof window.mailpoet_form_data;
|
||||
|
@ -2,6 +2,7 @@ import classnames from 'classnames';
|
||||
import { Component } from 'react';
|
||||
import jQuery from 'jquery';
|
||||
import PropTypes from 'prop-types';
|
||||
import { escapeHTML } from '@wordpress/escape-html';
|
||||
|
||||
import { Button } from 'common';
|
||||
import { Listing } from 'listing/listing.jsx';
|
||||
@ -155,7 +156,10 @@ const itemActions = [
|
||||
? response.data.name
|
||||
: MailPoet.I18n.t('noName');
|
||||
MailPoet.Notice.success(
|
||||
MailPoet.I18n.t('formDuplicated').replace('%1$s', formName),
|
||||
MailPoet.I18n.t('formDuplicated').replace(
|
||||
'%1$s',
|
||||
escapeHTML(formName),
|
||||
),
|
||||
);
|
||||
refresh();
|
||||
})
|
||||
@ -224,6 +228,20 @@ class FormListComponent extends Component {
|
||||
}
|
||||
|
||||
renderItem = (form, actions) => {
|
||||
if (form.settings === null) {
|
||||
MailPoet.Notice.error(
|
||||
MailPoet.I18n.t('formSettingsCorrupted')
|
||||
.replace('%1$s', escapeHTML(form.name))
|
||||
.replace(
|
||||
'[link]',
|
||||
`<a class="mailpoet-link" href="admin.php?page=mailpoet-form-editor&id=${parseInt(
|
||||
form.id,
|
||||
10,
|
||||
)}">`,
|
||||
)
|
||||
.replace('[/link]', '</a>'),
|
||||
);
|
||||
}
|
||||
const rowClasses = classnames(
|
||||
'manage-column',
|
||||
'column-primary',
|
||||
@ -249,7 +267,7 @@ class FormListComponent extends Component {
|
||||
</td>
|
||||
<td className="column" data-colname={MailPoet.I18n.t('segments')}>
|
||||
<SegmentTags segments={segments} dimension="large">
|
||||
{form.settings.segments_selected_by === 'user' && (
|
||||
{form.settings?.segments_selected_by === 'user' && (
|
||||
<span className="mailpoet-tags-prefix">
|
||||
{MailPoet.I18n.t('userChoice')}
|
||||
</span>
|
||||
|
30
mailpoet/assets/js/src/global.d.ts
vendored
30
mailpoet/assets/js/src/global.d.ts
vendored
@ -28,7 +28,7 @@ type MtaLog = {
|
||||
};
|
||||
|
||||
interface JQuery {
|
||||
parsley: () => any;
|
||||
parsley: (options?: { successClass?: string }) => any;
|
||||
mailpoetSerializeObject: () => {
|
||||
recaptchaWidgetId: number;
|
||||
token: string;
|
||||
@ -90,6 +90,8 @@ interface Window {
|
||||
mailpoet_premium_version: string;
|
||||
mailpoet_premium_link: string;
|
||||
mailpoet_woocommerce_active: boolean;
|
||||
mailpoet_woocommerce_version: string;
|
||||
mailpoet_track_wizard_loaded_via_woocommerce: boolean;
|
||||
mailpoet_premium_active: boolean;
|
||||
mailpoet_subscribers_limit: number;
|
||||
mailpoet_subscribers_limit_reached: boolean;
|
||||
@ -111,11 +113,11 @@ interface Window {
|
||||
mailpoet_wp_week_starts_on: number;
|
||||
mailpoet_subscribers_counts_cache_created_at: string;
|
||||
mailpoet_shortcode_links: string[];
|
||||
mailpoet_tracking_config: {
|
||||
mailpoet_tracking_config: Partial<{
|
||||
level: 'full' | 'partial' | 'basic';
|
||||
cookieTrackingEnabled: boolean;
|
||||
emailTrackingEnabled: boolean;
|
||||
};
|
||||
}>;
|
||||
mailpoet_display_detailed_stats: boolean;
|
||||
mailpoet_premium_plugin_installed: boolean;
|
||||
mailpoet_premium_plugin_download_url: string;
|
||||
@ -153,7 +155,6 @@ interface Window {
|
||||
}[];
|
||||
mailpoet_cdn_url: string;
|
||||
mailpoet_main_page_slug: string;
|
||||
sender_data: { name: string; address: string };
|
||||
finish_wizard_url: string;
|
||||
admin_email: string;
|
||||
wizard_sender_illustration_url: string;
|
||||
@ -174,6 +175,7 @@ interface Window {
|
||||
mailpoet_homepage_data: {
|
||||
taskListDismissed: boolean;
|
||||
productDiscoveryDismissed: boolean;
|
||||
upsellDismissed: boolean;
|
||||
taskListStatus: {
|
||||
senderSet: boolean;
|
||||
mssConnected: boolean;
|
||||
@ -187,9 +189,29 @@ interface Window {
|
||||
setUpAbandonedCartEmail: boolean;
|
||||
brandWooEmails: boolean;
|
||||
} | null;
|
||||
upsellStatus: {
|
||||
canDisplay: boolean;
|
||||
} | null;
|
||||
wooCustomersCount: number;
|
||||
subscribersCount: number;
|
||||
};
|
||||
templates?: Record<string, string>;
|
||||
is_wc_active?: boolean;
|
||||
systemInfoData?: Record<string, string>;
|
||||
mailpoet_mail_function_enabled: boolean;
|
||||
mailpoet_mss_key_pending_approval: boolean;
|
||||
mailpoet_show_congratulate_after_first_newsletter?: boolean;
|
||||
mailpoet_sender_address_field_blur?: () => void;
|
||||
mailpoet_woocommerce_transactional_email_id?: string;
|
||||
mailpoet_is_new_user?: boolean;
|
||||
mailpoet_editor_javascript_url?: string;
|
||||
mailpoet_woocommerce_automatic_emails?: Record<
|
||||
string,
|
||||
{
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
events: string[];
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import ReactDOM from 'react-dom';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { KnowledgeBase } from 'help/knowledge_base.jsx';
|
||||
import { SystemInfo } from 'help/system_info.jsx';
|
||||
import { SystemInfo } from 'help/system_info.tsx';
|
||||
import { SystemStatus } from 'help/system_status.jsx';
|
||||
import { YourPrivacy } from 'help/your_privacy.jsx';
|
||||
import { GlobalContext, useGlobalContextValue } from 'context/index.jsx';
|
||||
|
@ -1,40 +0,0 @@
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import _ from 'underscore';
|
||||
|
||||
function handleFocus(event) {
|
||||
event.target.select();
|
||||
}
|
||||
|
||||
function printData(data) {
|
||||
if (_.isObject(data)) {
|
||||
const printableData = Object.keys(data).map(
|
||||
(key) => `${key}: ${data[key]}`,
|
||||
);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
readOnly
|
||||
onFocus={handleFocus}
|
||||
value={printableData.join('\n')}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '400px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <p>{MailPoet.I18n.t('systemInfoDataError')}</p>;
|
||||
}
|
||||
|
||||
export function SystemInfo() {
|
||||
const systemInfoData = window.systemInfoData;
|
||||
return (
|
||||
<>
|
||||
<div className="mailpoet_notice notice inline">
|
||||
<p>{MailPoet.I18n.t('systemInfoIntro')}</p>
|
||||
</div>
|
||||
|
||||
{printData(systemInfoData)}
|
||||
</>
|
||||
);
|
||||
}
|
91
mailpoet/assets/js/src/help/system_info.tsx
Normal file
91
mailpoet/assets/js/src/help/system_info.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { useState } from 'react';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import _ from 'underscore';
|
||||
import { Notice } from '../notices/notice';
|
||||
import { Button } from '../common';
|
||||
|
||||
function handleFocus(event) {
|
||||
event.target.select();
|
||||
}
|
||||
|
||||
function printData(data: Record<string, string> | undefined, id: string) {
|
||||
if (_.isObject(data)) {
|
||||
const printableData = Object.keys(data).map(
|
||||
(key) => `${key}: ${data[key]}`,
|
||||
);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
readOnly
|
||||
id={id}
|
||||
onFocus={handleFocus}
|
||||
value={printableData.join('\n')}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '400px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <p>{MailPoet.I18n.t('systemInfoDataError')}</p>;
|
||||
}
|
||||
|
||||
async function copyToClipboard(
|
||||
id: string,
|
||||
resultCallback: (success: boolean) => void,
|
||||
) {
|
||||
const element: HTMLTextAreaElement | null = document.querySelector(`#${id}`);
|
||||
if (!element) {
|
||||
resultCallback(false);
|
||||
return;
|
||||
}
|
||||
if (navigator.clipboard) {
|
||||
const text = element.value;
|
||||
await navigator.clipboard.writeText(text);
|
||||
resultCallback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback if navigator.clipboard does not work.
|
||||
element.focus();
|
||||
element.select();
|
||||
if (document.execCommand('copy')) {
|
||||
resultCallback(true);
|
||||
return;
|
||||
}
|
||||
resultCallback(false);
|
||||
}
|
||||
|
||||
export function SystemInfo() {
|
||||
const [copySuccess, setCopySuccess] = useState(null);
|
||||
const id = 'mailpoet-system-info';
|
||||
|
||||
const systemInfoData = window.systemInfoData;
|
||||
return (
|
||||
<>
|
||||
<div className="mailpoet_notice notice inline">
|
||||
<p>{MailPoet.I18n.t('systemInfoIntro')}</p>
|
||||
</div>
|
||||
|
||||
{printData(systemInfoData, id)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
void copyToClipboard(id, setCopySuccess);
|
||||
}}
|
||||
>
|
||||
{MailPoet.I18n.t('copyToClipboard')}
|
||||
</Button>
|
||||
{copySuccess === true && (
|
||||
<Notice type="info">
|
||||
<p>{MailPoet.I18n.t('copyToClipboardSuccess')}</p>
|
||||
</Notice>
|
||||
)}
|
||||
{copySuccess === false && (
|
||||
<Notice type="warning">
|
||||
<p>{MailPoet.I18n.t('copyToClipboardFailure')}</p>
|
||||
</Notice>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,19 +1,28 @@
|
||||
import { ErrorBoundary } from 'common';
|
||||
import { TaskList } from 'homepage/components/task-list';
|
||||
import { ProductDiscovery } from 'homepage/components/product-discovery';
|
||||
import { Upsell } from 'homepage/components/upsell';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { storeName } from 'homepage/store/store';
|
||||
|
||||
export function HomepageSections(): JSX.Element {
|
||||
const { isTaskListHidden, isProductDiscoveryHidden } = useSelect(
|
||||
const {
|
||||
isTaskListHidden,
|
||||
isProductDiscoveryHidden,
|
||||
isUpsellHidden,
|
||||
canDisplayUpsell,
|
||||
} = useSelect(
|
||||
(select) => ({
|
||||
isTaskListHidden: select(storeName).getIsTaskListHidden(),
|
||||
isProductDiscoveryHidden: select(storeName).getIsProductDiscoveryHidden(),
|
||||
isUpsellHidden: select(storeName).getIsUpsellHidden(),
|
||||
canDisplayUpsell: select(storeName).getCanDisplayUpsell(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const { hideTaskList } = useDispatch(storeName);
|
||||
const { hideProductDiscovery } = useDispatch(storeName);
|
||||
const { hideUpsell } = useDispatch(storeName);
|
||||
return (
|
||||
<div className="mailpoet-homepage__container">
|
||||
{!isTaskListHidden ? (
|
||||
@ -26,6 +35,14 @@ export function HomepageSections(): JSX.Element {
|
||||
<ProductDiscovery onHide={hideProductDiscovery} />
|
||||
</ErrorBoundary>
|
||||
) : null}
|
||||
{isTaskListHidden &&
|
||||
isProductDiscoveryHidden &&
|
||||
canDisplayUpsell &&
|
||||
!isUpsellHidden ? (
|
||||
<ErrorBoundary>
|
||||
<Upsell closable onHide={hideUpsell} />
|
||||
</ErrorBoundary>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
69
mailpoet/assets/js/src/homepage/components/upsell.tsx
Normal file
69
mailpoet/assets/js/src/homepage/components/upsell.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import {
|
||||
closeSmall,
|
||||
lifesaver,
|
||||
megaphone,
|
||||
people,
|
||||
trendingUp,
|
||||
} from '@wordpress/icons';
|
||||
import { Button, Icon } from '@wordpress/components';
|
||||
import { ContentSection } from './content-section';
|
||||
|
||||
type Props = {
|
||||
closable: boolean;
|
||||
onHide?: () => void;
|
||||
};
|
||||
|
||||
export function Upsell({ closable, onHide }: Props): JSX.Element {
|
||||
return (
|
||||
<ContentSection
|
||||
className="mailpoet-homepage-upsell"
|
||||
heading={MailPoet.I18n.t('accelerateYourGrowth')}
|
||||
headingAfter={
|
||||
closable && onHide ? (
|
||||
<Button
|
||||
icon={closeSmall}
|
||||
onClick={onHide}
|
||||
label={MailPoet.I18n.t('close')}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<div className="mailpoet-homepage-upsell__content">
|
||||
<ul>
|
||||
<li>
|
||||
<Icon icon={trendingUp} />
|
||||
<span>{MailPoet.I18n.t('detailedAnalytics')}</span>
|
||||
</li>
|
||||
<li>
|
||||
<Icon icon={people} />
|
||||
<span>{MailPoet.I18n.t('advancedSubscriberSegmentation')}</span>
|
||||
</li>
|
||||
<li>
|
||||
<Icon icon={megaphone} />
|
||||
<span>{MailPoet.I18n.t('emailMarketingAutomations')}</span>
|
||||
</li>
|
||||
<li>
|
||||
<Icon icon={lifesaver} />
|
||||
<span>{MailPoet.I18n.t('prioritySupport')}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Button
|
||||
variant="primary"
|
||||
href={MailPoet.MailPoetComUrlFactory.getPurchasePlanUrl(
|
||||
MailPoet.subscribersCount,
|
||||
MailPoet.currentWpUserEmail,
|
||||
'business',
|
||||
{
|
||||
utm_source: 'plugin',
|
||||
utm_medium: 'homepage',
|
||||
utm_campaign: 'upsell',
|
||||
},
|
||||
)}
|
||||
>
|
||||
{MailPoet.I18n.t('upgradePlan')}
|
||||
</Button>
|
||||
</div>
|
||||
</ContentSection>
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
saveProductDiscoveryDismissed,
|
||||
saveTaskListDismissed,
|
||||
saveUpsellDismissed,
|
||||
} from 'homepage/store/controls';
|
||||
|
||||
export function* hideTaskList() {
|
||||
@ -12,3 +13,8 @@ export function* hideProductDiscovery() {
|
||||
yield saveProductDiscoveryDismissed();
|
||||
return { type: 'SET_PRODUCT_DISCOVERY_HIDDEN' };
|
||||
}
|
||||
|
||||
export function* hideUpsell() {
|
||||
yield saveUpsellDismissed();
|
||||
return { type: 'SET_UPSELL_HIDDEN' };
|
||||
}
|
||||
|
@ -21,3 +21,14 @@ export function saveProductDiscoveryDismissed() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function saveUpsellDismissed() {
|
||||
return callApi({
|
||||
endpoint: 'settings',
|
||||
action: 'set',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'homepage.upsell_dismissed': true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -15,6 +15,10 @@ export function getInitialState(): State {
|
||||
isHidden: window.mailpoet_homepage_data.productDiscoveryDismissed,
|
||||
tasksStatus: window.mailpoet_homepage_data.productDiscoveryStatus,
|
||||
},
|
||||
upsell: {
|
||||
isHidden: window.mailpoet_homepage_data.upsellDismissed,
|
||||
upsellStatus: window.mailpoet_homepage_data.upsellStatus,
|
||||
},
|
||||
isWooCommerceActive: MailPoet.isWoocommerceActive,
|
||||
};
|
||||
}
|
||||
|
@ -19,6 +19,14 @@ export function reducer(state: State, action: Action): State {
|
||||
isHidden: true,
|
||||
},
|
||||
};
|
||||
case 'SET_UPSELL_HIDDEN':
|
||||
return {
|
||||
...state,
|
||||
upsell: {
|
||||
...state.upsell,
|
||||
isHidden: true,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -63,3 +63,11 @@ export function getCurrentTask(state: State): TaskType | null {
|
||||
if (!state.taskList.tasksStatus.subscribersAdded) return 'subscribersAdded';
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getIsUpsellHidden(state: State): boolean {
|
||||
return state.upsell.isHidden;
|
||||
}
|
||||
|
||||
export function getCanDisplayUpsell(state: State): boolean {
|
||||
return state.upsell.upsellStatus?.canDisplay;
|
||||
}
|
||||
|
@ -27,8 +27,18 @@ export type ProductDiscoveryTasksStatus = {
|
||||
|
||||
export type TaskType = keyof TaskListTasksStatus;
|
||||
|
||||
export type UpsellStatus = {
|
||||
canDisplay: boolean;
|
||||
};
|
||||
|
||||
export type UpsellState = {
|
||||
isHidden: boolean;
|
||||
upsellStatus: UpsellStatus;
|
||||
};
|
||||
|
||||
export type State = {
|
||||
taskList: TaskListState;
|
||||
productDiscovery: ProductDiscoveryState;
|
||||
upsell: UpsellState;
|
||||
isWooCommerceActive: boolean;
|
||||
};
|
||||
|
88
mailpoet/assets/js/src/landingpage/ab-test-button.tsx
Normal file
88
mailpoet/assets/js/src/landingpage/ab-test-button.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import {
|
||||
Experiment,
|
||||
Variant,
|
||||
emitter,
|
||||
experimentDebugger,
|
||||
} from '@marvelapp/react-ab-test';
|
||||
import { Button } from 'common';
|
||||
import {
|
||||
MailPoetTrackEvent,
|
||||
CacheEventOptionSaveInStorage,
|
||||
} from '../analytics_event';
|
||||
import { redirectToWelcomeWizard } from './util';
|
||||
|
||||
const EXPERIMENT_NAME = 'landing_page_cta_display';
|
||||
const VARIANT_BEGIN_SETUP = 'landing_page_cta_display_variant_begin_setup';
|
||||
const VARIANT_GET_STARTED_FOR_FREE =
|
||||
'landing_page_cta_display_variant_get_started_for_free';
|
||||
|
||||
// analytics permission is currently unavailable at this point
|
||||
// we will save the event data and send them to mixpanel later
|
||||
// details in MAILPOET-4972
|
||||
|
||||
// Called when the experiment is displayed to the user.
|
||||
emitter.addPlayListener((experimentName, variantName) => {
|
||||
MailPoetTrackEvent(
|
||||
'Experiment Display',
|
||||
{
|
||||
experiment: experimentName,
|
||||
variant: variantName,
|
||||
},
|
||||
CacheEventOptionSaveInStorage, // persist in local storage
|
||||
);
|
||||
});
|
||||
|
||||
// Called when a 'win' is emitted
|
||||
emitter.addWinListener((experimentName, variantName) => {
|
||||
MailPoetTrackEvent(
|
||||
'Experiment Win',
|
||||
{
|
||||
experiment: experimentName,
|
||||
variant: variantName,
|
||||
},
|
||||
CacheEventOptionSaveInStorage, // persist in local storage
|
||||
redirectToWelcomeWizard, // callback
|
||||
);
|
||||
});
|
||||
|
||||
// Define variants in advance.
|
||||
emitter.defineVariants(
|
||||
EXPERIMENT_NAME,
|
||||
[VARIANT_BEGIN_SETUP, VARIANT_GET_STARTED_FOR_FREE],
|
||||
[50, 50],
|
||||
);
|
||||
|
||||
experimentDebugger.setDebuggerAvailable(
|
||||
MailPoet.FeaturesController.isSupported('landingpage_ab_test_debugger'),
|
||||
);
|
||||
experimentDebugger.enable();
|
||||
|
||||
function AbTestButton() {
|
||||
return (
|
||||
<Experiment name={EXPERIMENT_NAME}>
|
||||
<Variant name={VARIANT_BEGIN_SETUP}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
emitter.emitWin(EXPERIMENT_NAME);
|
||||
}}
|
||||
>
|
||||
{__('Begin setup', 'mailpoet')}
|
||||
</Button>
|
||||
</Variant>
|
||||
<Variant name={VARIANT_GET_STARTED_FOR_FREE}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
emitter.emitWin(EXPERIMENT_NAME);
|
||||
}}
|
||||
>
|
||||
{__('Get started for free', 'mailpoet')}
|
||||
</Button>
|
||||
</Variant>
|
||||
</Experiment>
|
||||
);
|
||||
}
|
||||
AbTestButton.displayName = 'Landingpage Ab Test';
|
||||
|
||||
export { AbTestButton };
|
@ -1,7 +1,6 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button } from 'common';
|
||||
import { Heading } from 'common/typography/heading/heading';
|
||||
import { redirectToWelcomeWizard } from './util';
|
||||
import { AbTestButton } from './ab-test-button';
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
@ -11,9 +10,7 @@ function Footer() {
|
||||
{' '}
|
||||
{__('Ready to start using MailPoet?', 'mailpoet')}{' '}
|
||||
</Heading>
|
||||
<Button onClick={redirectToWelcomeWizard}>
|
||||
{__('Begin setup', 'mailpoet')}
|
||||
</Button>
|
||||
<AbTestButton />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button } from 'common';
|
||||
import { Heading } from 'common/typography/heading/heading';
|
||||
import { redirectToWelcomeWizard } from './util';
|
||||
import { AbTestButton } from './ab-test-button';
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
@ -16,9 +15,7 @@ function Header() {
|
||||
'mailpoet',
|
||||
)}
|
||||
</p>
|
||||
<Button onClick={redirectToWelcomeWizard}>
|
||||
{__('Begin setup', 'mailpoet')}
|
||||
</Button>
|
||||
<AbTestButton />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
@ -2,6 +2,8 @@ import ReactDOM from 'react-dom';
|
||||
import { GlobalContext, useGlobalContextValue } from 'context/index.jsx';
|
||||
import { ErrorBoundary } from 'common';
|
||||
import { Background } from 'common/background/background';
|
||||
import { HideScreenOptions } from 'common/hide_screen_options/hide_screen_options';
|
||||
import { TopBarWithBeamer } from 'common/top_bar/top_bar';
|
||||
import { Header } from './header';
|
||||
import { Footer } from './footer';
|
||||
import { Faq } from './faq';
|
||||
@ -11,6 +13,9 @@ function Landingpage() {
|
||||
return (
|
||||
<GlobalContext.Provider value={useGlobalContextValue(window)}>
|
||||
<main>
|
||||
<HideScreenOptions />
|
||||
<TopBarWithBeamer />
|
||||
|
||||
<Background color="#fff" />
|
||||
|
||||
<Header />
|
||||
|
@ -5,8 +5,11 @@ import { MailPoetDate } from './date';
|
||||
import { MailPoetAjax } from './ajax';
|
||||
import { MailPoetModal } from './modal';
|
||||
import { MailPoetNotice } from './notice';
|
||||
// side effect - extends MailPoet object in initializeMixpanelWhenLoaded
|
||||
import { MailPoetForceTrackEvent, MailPoetTrackEvent } from './analytics_event';
|
||||
import {
|
||||
MailPoetForceTrackEvent,
|
||||
MailPoetTrackEvent,
|
||||
initializeMixpanelWhenLoaded,
|
||||
} from './analytics_event';
|
||||
import { MailPoetNum } from './num';
|
||||
import { MailPoetHelpTooltip } from './help-tooltip-helper';
|
||||
import { MailPoetIframe } from './iframe';
|
||||
@ -71,6 +74,7 @@ export const MailPoet = {
|
||||
transactionalEmailsEnabled: window.mailpoet_send_transactional_emails,
|
||||
transactionalEmailsOptInNoticeDismissed:
|
||||
window.mailpoet_transactional_emails_opt_in_notice_dismissed,
|
||||
mailFunctionEnabled: window.mailpoet_mail_function_enabled,
|
||||
} as const;
|
||||
|
||||
declare global {
|
||||
@ -81,3 +85,6 @@ declare global {
|
||||
|
||||
// Expose MailPoet globally
|
||||
window.MailPoet = MailPoet;
|
||||
|
||||
// initializeMixpanelWhenLoaded needs to be called after window.MailPoet is defined.
|
||||
initializeMixpanelWhenLoaded();
|
||||
|
@ -316,10 +316,7 @@ BL.MediaManagerBehavior = Marionette.Behavior.extend({
|
||||
width: mainSize.width + 'px',
|
||||
src: mainSize.url,
|
||||
alt:
|
||||
attachment.get('alt') !== '' &&
|
||||
attachment.get('alt') !== undefined
|
||||
? attachment.get('alt')
|
||||
: attachment.get('title'),
|
||||
attachment.get('alt') !== undefined ? attachment.get('alt') : '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -7,7 +7,8 @@ import { BaseBlock } from 'newsletter_editor/blocks/base';
|
||||
import _ from 'underscore';
|
||||
import jQuery from 'jquery';
|
||||
import 'backbone.marionette';
|
||||
import { MailPoet } from '../../mailpoet';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { CommunicationComponent } from 'newsletter_editor/components/communication';
|
||||
|
||||
export const FEATURE_COUPON_BLOCK = 'Coupon block';
|
||||
|
||||
@ -17,7 +18,36 @@ const base = BaseBlock;
|
||||
Module.CouponBlockModel = base.BlockModel.extend({
|
||||
defaults() {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return this._getDefaults({}, App.getConfig().get('blockDefaults.coupon'));
|
||||
return this._getDefaults(
|
||||
{
|
||||
productIds: [], // selected product ids,
|
||||
excludedProductIds: [],
|
||||
productCategories: [], // selected categories id
|
||||
excludedProductCategories: [],
|
||||
type: 'coupon',
|
||||
amount: 10,
|
||||
amountMax: 100,
|
||||
discountType: 'percent',
|
||||
expiryDay: 10,
|
||||
styles: {
|
||||
block: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#000000',
|
||||
borderRadius: '5px',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px',
|
||||
fontColor: '#000000',
|
||||
fontFamily: 'Verdan',
|
||||
fontSize: '18px',
|
||||
fontWeight: 'normal',
|
||||
lineHeight: '40px',
|
||||
textAlign: 'center',
|
||||
width: '200px',
|
||||
},
|
||||
},
|
||||
},
|
||||
App.getConfig().get('blockDefaults.coupon'),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@ -56,15 +86,48 @@ Module.CouponBlockSettingsView = base.BlockSettingsView.extend({
|
||||
events() {
|
||||
return {
|
||||
'input .mailpoet_field_coupon_code': _.partial(this.changeField, 'code'),
|
||||
'change .mailpoet_field_coupon_source': 'changeSource',
|
||||
'change .mailpoet_field_coupon_discount_type': 'changeDiscountType',
|
||||
'input .mailpoet_field_coupon_amount': _.partial(
|
||||
this.changeField,
|
||||
this.validateChangeField,
|
||||
'amount',
|
||||
),
|
||||
'input .mailpoet_field_coupon_expiry_day': _.partial(
|
||||
this.changeField,
|
||||
this.validateChangeField,
|
||||
'expiryDay',
|
||||
),
|
||||
'change .mailpoet_field_coupon_free_shipping': _.partial(
|
||||
this.changeBoolCheckboxField,
|
||||
'freeShipping',
|
||||
),
|
||||
'input .mailpoet_field_coupon_minimum_amount': _.partial(
|
||||
this.validateMinAndMaxAmountFields,
|
||||
'minimumAmount',
|
||||
),
|
||||
'input .mailpoet_field_coupon_maximum_amount': _.partial(
|
||||
this.validateMinAndMaxAmountFields,
|
||||
'maximumAmount',
|
||||
),
|
||||
'change .mailpoet_field_coupon_individual_use': _.partial(
|
||||
this.changeBoolCheckboxField,
|
||||
'individualUse',
|
||||
),
|
||||
'change .mailpoet_field_coupon_exclude_sale_items': _.partial(
|
||||
this.changeBoolCheckboxField,
|
||||
'excludeSaleItems',
|
||||
),
|
||||
'input .mailpoet_field_coupon_email_restrictions': _.partial(
|
||||
this.validateEmailRestrictionsField,
|
||||
'emailRestrictions',
|
||||
),
|
||||
'input .mailpoet_field_coupon_usage_limit': _.partial(
|
||||
this.changeField,
|
||||
'usageLimit',
|
||||
),
|
||||
'input .mailpoet_field_coupon_usage_limit_per_user': _.partial(
|
||||
this.changeField,
|
||||
'usageLimitPerUser',
|
||||
),
|
||||
'change .mailpoet_field_coupon_alignment': _.partial(
|
||||
this.changeField,
|
||||
'styles.block.textAlign',
|
||||
@ -175,6 +238,15 @@ Module.CouponBlockSettingsView = base.BlockSettingsView.extend({
|
||||
availableDiscountTypes: App.getConfig()
|
||||
.get('coupon.discount_types')
|
||||
.toJSON(),
|
||||
availableCoupons: App.getConfig()
|
||||
.get('coupon.available_coupons')
|
||||
.toJSON(),
|
||||
minAndMaxAmountFieldsErrorMessage: MailPoet.I18n.t(
|
||||
'couponMinAndMaxAmountFieldsErrorMessage',
|
||||
).replace(
|
||||
'%s',
|
||||
String(App.getConfig().get('coupon.price_decimal_separator')),
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -218,6 +290,16 @@ Module.CouponBlockSettingsView = base.BlockSettingsView.extend({
|
||||
// It's a new element after the re-render
|
||||
this.$('.mailpoet_field_coupon_amount').parsley().validate();
|
||||
},
|
||||
validateChangeField(field, event) {
|
||||
const element = this.$(event.target);
|
||||
const value = element.val();
|
||||
|
||||
if (!element.parsley().isValid()) {
|
||||
return; // input invalid. not saving
|
||||
}
|
||||
|
||||
this.model.set(field, value);
|
||||
},
|
||||
onRender() {
|
||||
this.$('[data-parsley-validate]')
|
||||
.parsley()
|
||||
@ -226,6 +308,257 @@ Module.CouponBlockSettingsView = base.BlockSettingsView.extend({
|
||||
instance.validate();
|
||||
}
|
||||
});
|
||||
|
||||
const model = this.model;
|
||||
this.$('.mailpoet_field_coupon_existing_coupon')
|
||||
.select2({
|
||||
multiple: false,
|
||||
allowClear: false,
|
||||
})
|
||||
.on({
|
||||
'select2:select': function (event) {
|
||||
const couponId = event.params.data.id;
|
||||
const couponCode = event.params.data.text;
|
||||
model.set('couponId', couponId);
|
||||
model.set('code', couponCode);
|
||||
},
|
||||
})
|
||||
.trigger('change');
|
||||
|
||||
const fieldKeys = {
|
||||
productIds: 'productIds',
|
||||
excludedProductIds: 'excludedProductIds',
|
||||
productCategories: 'productCategories',
|
||||
excludedProductCategories: 'excludedProductCategories',
|
||||
};
|
||||
|
||||
this.$('#mailpoet_field_coupon_product_ids')
|
||||
.select2(this.productSelect2Options())
|
||||
.on(this.select2OnEventOptions(fieldKeys.productIds))
|
||||
.trigger('change');
|
||||
|
||||
this.$('#mailpoet_field_coupon_excluded_product_ids')
|
||||
.select2(this.productSelect2Options())
|
||||
.on(this.select2OnEventOptions(fieldKeys.excludedProductIds))
|
||||
.trigger('change');
|
||||
|
||||
this.$('#mailpoet_field_coupon_product_categories')
|
||||
.select2(this.categoriesSelect2Options())
|
||||
.on(this.select2OnEventOptions(fieldKeys.productCategories))
|
||||
.trigger('change');
|
||||
|
||||
this.$('#mailpoet_field_coupon_excluded_product_categories')
|
||||
.select2(this.categoriesSelect2Options())
|
||||
.on(this.select2OnEventOptions(fieldKeys.excludedProductCategories))
|
||||
.trigger('change');
|
||||
},
|
||||
changeSource(event) {
|
||||
const value = jQuery(event.target).val();
|
||||
this.model.set('source', value);
|
||||
|
||||
if (value === 'createNew') {
|
||||
this.$('.mailpoet_field_coupon_source_use_existing').addClass(
|
||||
'mailpoet_hidden',
|
||||
);
|
||||
this.$('.mailpoet_field_coupon_source_create_new').removeClass(
|
||||
'mailpoet_hidden',
|
||||
);
|
||||
// reset code placeholder
|
||||
this.model.set('code', App.getConfig().get('coupon.code_placeholder'));
|
||||
this.model.set('couponId', null);
|
||||
} else if (value === 'useExisting') {
|
||||
this.$('.mailpoet_field_coupon_source_create_new').addClass(
|
||||
'mailpoet_hidden',
|
||||
);
|
||||
this.$('.mailpoet_field_coupon_source_use_existing').removeClass(
|
||||
'mailpoet_hidden',
|
||||
);
|
||||
// set selected code from available
|
||||
this.model.set(
|
||||
'code',
|
||||
this.$('.mailpoet_field_coupon_existing_coupon')
|
||||
.find(':selected')
|
||||
.text(),
|
||||
);
|
||||
this.model.set(
|
||||
'couponId',
|
||||
this.$('.mailpoet_field_coupon_existing_coupon')
|
||||
.find(':selected')
|
||||
.val(),
|
||||
);
|
||||
}
|
||||
},
|
||||
productSelect2Options() {
|
||||
const productOptions = {
|
||||
type: 'products',
|
||||
amount: '10', // number of fetched products
|
||||
offset: 0,
|
||||
contentType: 'product',
|
||||
postStatus: 'publish', // 'draft'|'pending'|'publish'
|
||||
search: '', // Search keyword term
|
||||
|
||||
sortBy: 'newest', // 'newest'|'oldest',
|
||||
};
|
||||
|
||||
return {
|
||||
multiple: true,
|
||||
allowClear: true,
|
||||
ajax: {
|
||||
delay: 250, // wait 250 milliseconds before triggering the request
|
||||
|
||||
data: (params) => {
|
||||
const currentPage = params.page || 1;
|
||||
return {
|
||||
search: params.term,
|
||||
page: currentPage,
|
||||
// page starts from 1, offset starts from 0. we need to perform some calc to make sure it retrieve the right number of results
|
||||
// 'query:append' is added during pagination
|
||||
offset:
|
||||
params._type === 'query:append' // eslint-disable-line no-underscore-dangle
|
||||
? (currentPage - 1) * Number(productOptions.amount)
|
||||
: 0,
|
||||
};
|
||||
},
|
||||
transport: (options, success, failure) => {
|
||||
// Fetch available products
|
||||
const productPromise = CommunicationComponent.getPosts({
|
||||
...productOptions,
|
||||
search: options.data.search,
|
||||
offset: options.data.offset,
|
||||
});
|
||||
|
||||
productPromise.then(success);
|
||||
productPromise.fail(failure);
|
||||
return productPromise;
|
||||
},
|
||||
processResults: (data) => ({
|
||||
results: _.map(data, (item) =>
|
||||
_.defaults(
|
||||
{
|
||||
text: item.post_title,
|
||||
id: item.ID,
|
||||
},
|
||||
item,
|
||||
),
|
||||
),
|
||||
pagination: {
|
||||
more: data.length >= Number(productOptions.amount),
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
categoriesSelect2Options() {
|
||||
return {
|
||||
multiple: true,
|
||||
allowClear: true,
|
||||
ajax: {
|
||||
delay: 250, // wait 250 milliseconds before triggering the request
|
||||
data: (params) => ({
|
||||
search: params.term,
|
||||
page: params.page || 1,
|
||||
}),
|
||||
transport: (options, success, failure) => {
|
||||
// Fetch available product categories
|
||||
const termsPromise = CommunicationComponent.getTerms({
|
||||
search: options.data.search,
|
||||
page: options.data.page,
|
||||
taxonomies: ['product_cat'],
|
||||
});
|
||||
termsPromise.then(success);
|
||||
termsPromise.fail(failure);
|
||||
return termsPromise;
|
||||
},
|
||||
processResults: (data) => ({
|
||||
results: _.map(data, (item) =>
|
||||
_.defaults(
|
||||
{
|
||||
text: item.name,
|
||||
id: item.term_id,
|
||||
},
|
||||
item,
|
||||
),
|
||||
),
|
||||
pagination: {
|
||||
more: data.length === 100,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
select2OnEventOptions(fieldName) {
|
||||
return {
|
||||
'select2:select': (event) => {
|
||||
const modelItem = this.model.get(fieldName);
|
||||
modelItem.add(event.params.data);
|
||||
// Reset whole model in order for change events to propagate properly
|
||||
this.model.set(fieldName, modelItem.toJSON());
|
||||
},
|
||||
'select2:unselect': (event) => {
|
||||
const modelItem = this.model.get(fieldName);
|
||||
modelItem.remove(event.params.data);
|
||||
// Reset whole model in order for change events to propagate properly
|
||||
this.model.set(fieldName, modelItem.toJSON());
|
||||
},
|
||||
};
|
||||
},
|
||||
validateMinAndMaxAmountFields(field, event) {
|
||||
const element = event.target;
|
||||
const errorElem = element.nextElementSibling;
|
||||
|
||||
// this validation code was gotten from https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js#L150-L212
|
||||
// used by /wp-admin/edit.php?post_type=shop_coupon :- when adding/editing a coupon
|
||||
|
||||
const priceDecimalSeparator = String(
|
||||
App.getConfig().get('coupon.price_decimal_separator'),
|
||||
);
|
||||
|
||||
const regex = new RegExp(`[^-0-9%\\${priceDecimalSeparator}]+`, 'gi');
|
||||
const decimalRegex = new RegExp(`[^\\${priceDecimalSeparator}]`, 'gi');
|
||||
|
||||
const value = element.value;
|
||||
let newvalue = value.replace(regex, '');
|
||||
|
||||
// Check if newvalue have more than one decimal point.
|
||||
if (newvalue.replace(decimalRegex, '').length > 1) {
|
||||
newvalue = newvalue.replace(decimalRegex, '');
|
||||
}
|
||||
|
||||
if (value !== newvalue) {
|
||||
// show error message
|
||||
if (errorElem && errorElem.classList.contains('mailpoet_hidden')) {
|
||||
errorElem.classList.remove('mailpoet_hidden');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (errorElem && !errorElem.classList.contains('mailpoet_hidden')) {
|
||||
errorElem.classList.add('mailpoet_hidden'); // hide error message
|
||||
}
|
||||
|
||||
this.model.set(field, value);
|
||||
},
|
||||
validateEmailRestrictionsField(field, event) {
|
||||
const element = event.target;
|
||||
const errorElem = element.nextElementSibling;
|
||||
|
||||
const isValid = element.checkValidity();
|
||||
|
||||
if (!isValid) {
|
||||
errorElem.textContent = element.validationMessage;
|
||||
|
||||
if (errorElem.classList.contains('mailpoet_hidden')) {
|
||||
errorElem.classList.remove('mailpoet_hidden');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (errorElem && !errorElem.classList.contains('mailpoet_hidden')) {
|
||||
errorElem.classList.add('mailpoet_hidden');
|
||||
}
|
||||
|
||||
this.model.set(field, element.value);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -6,7 +6,7 @@ import Marionette from 'backbone.marionette';
|
||||
import $ from 'jquery';
|
||||
import Blob from 'blob';
|
||||
import FileSaver from 'file-saver';
|
||||
import * as Thumbnail from 'common/thumbnail.ts';
|
||||
import { isTruthy, fromNewsletter } from 'common';
|
||||
import _ from 'underscore';
|
||||
import SuperModel from 'backbone.supermodel/build/backbone.supermodel';
|
||||
|
||||
@ -99,7 +99,7 @@ Module.save = function () {
|
||||
};
|
||||
|
||||
Module.saveTemplate = function (options) {
|
||||
return Thumbnail.fromNewsletter(App.toJSON()).then(function (thumbnail) {
|
||||
return fromNewsletter(App.toJSON()).then(function (thumbnail) {
|
||||
var data = _.extend(options || {}, {
|
||||
thumbnail_data: thumbnail,
|
||||
body: JSON.stringify(App.getBody()),
|
||||
@ -116,7 +116,7 @@ Module.saveTemplate = function (options) {
|
||||
};
|
||||
|
||||
Module.exportTemplate = function (options) {
|
||||
return Thumbnail.fromNewsletter(App.toJSON()).then(function (thumbnail) {
|
||||
return fromNewsletter(App.toJSON()).then(function (thumbnail) {
|
||||
var data = _.extend(options || {}, {
|
||||
thumbnail_data: thumbnail,
|
||||
body: App.getBody(),
|
||||
@ -573,6 +573,9 @@ Module.NewsletterPreviewModel = SuperModel.extend({
|
||||
previewSendingError: false,
|
||||
previewSendingSuccess: false,
|
||||
sendingPreview: false,
|
||||
mssPendingApproval: window.mailpoet_mss_key_pending_approval,
|
||||
mssKeyPendingApprovalRefreshMessage: true,
|
||||
awaitingKeyCheck: false,
|
||||
},
|
||||
});
|
||||
|
||||
@ -588,6 +591,7 @@ Module.NewsletterPreviewView = Marionette.View.extend({
|
||||
return {
|
||||
'change .mailpoet_browser_preview_type': 'changeBrowserPreviewType',
|
||||
'click #mailpoet_send_preview': 'sendPreview',
|
||||
'click #refresh-mss-key-status': 'refreshMssKeyStatus',
|
||||
};
|
||||
},
|
||||
initialize: function (options) {
|
||||
@ -606,7 +610,11 @@ Module.NewsletterPreviewView = Marionette.View.extend({
|
||||
this.$('#mailpoet_preview_to_email').val() || window.currentUserEmail,
|
||||
previewSendingError: this.model.get('previewSendingError'),
|
||||
sendingPreview: this.model.get('sendingPreview'),
|
||||
mssKeyPendingApproval: window.mailpoet_mss_key_pending_approval,
|
||||
mssKeyPendingApproval: this.model.get('mssPendingApproval'),
|
||||
mssKeyPendingApprovalRefreshMessage: this.model.get(
|
||||
'mssKeyPendingApprovalRefreshMessage',
|
||||
),
|
||||
awaitingKeyCheck: this.model.get('awaitingKeyCheck'),
|
||||
};
|
||||
},
|
||||
changeBrowserPreviewType: function (event) {
|
||||
@ -712,11 +720,11 @@ Module.NewsletterPreviewView = Marionette.View.extend({
|
||||
)}</p>
|
||||
<p>
|
||||
<a
|
||||
href=${MailPoet.MailPoetComUrlFactory.getFreePlanUrl({
|
||||
href='${MailPoet.MailPoetComUrlFactory.getFreePlanUrl({
|
||||
utm_campaign: 'sending-error',
|
||||
})}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
})}'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
${MailPoet.I18n.t(
|
||||
'newsletterPreviewErrorSignUpForSendingService',
|
||||
@ -744,6 +752,33 @@ Module.NewsletterPreviewView = Marionette.View.extend({
|
||||
});
|
||||
return undefined;
|
||||
},
|
||||
refreshMssKeyStatus: function () {
|
||||
this.model.set('awaitingKeyCheck', true);
|
||||
|
||||
return MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'services',
|
||||
action: 'refreshMSSKeyStatus',
|
||||
})
|
||||
.done((response) => {
|
||||
this.model.set('awaitingKeyCheck', false);
|
||||
if (response.data && response.data.result.code === 200) {
|
||||
this.model.set(
|
||||
'mssPendingApproval',
|
||||
!isTruthy(response.data.result.data.is_approved),
|
||||
);
|
||||
this.model.set('mssKeyPendingApprovalRefreshMessage', false);
|
||||
}
|
||||
})
|
||||
.fail((response) => {
|
||||
this.model.set('awaitingKeyCheck', false);
|
||||
if (response.errors && Array.isArray(response.errors)) {
|
||||
const messages = response.errors.map((e) => e.message);
|
||||
const errorEl = document.querySelector('.pendindig_approval_error');
|
||||
errorEl.innerHTML = messages.join('\n');
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
App.on('before:start', function (BeforeStartApp) {
|
||||
|
@ -6,6 +6,7 @@ export enum NewsletterType {
|
||||
NotificationHistory = 'notification_history',
|
||||
WCTransactional = 'wc_transactional',
|
||||
ReEngagement = 're_engagement',
|
||||
Automation = 'automation',
|
||||
}
|
||||
|
||||
export enum NewsletterStatus {
|
||||
@ -16,6 +17,10 @@ export enum NewsletterStatus {
|
||||
Active = 'active',
|
||||
}
|
||||
|
||||
export enum NewsletterOptionGroup {
|
||||
WooCommerce = 'woocommerce',
|
||||
}
|
||||
|
||||
export type NewsLetter = {
|
||||
body: {
|
||||
blockDefaults: unknown;
|
||||
@ -39,13 +44,19 @@ export type NewsLetter = {
|
||||
isScheduled: string;
|
||||
scheduledAt: string;
|
||||
disabled?: string;
|
||||
group: NewsletterOptionGroup;
|
||||
intervalType?: string;
|
||||
event: string;
|
||||
automationId?: string;
|
||||
afterTimeNumber: number | string;
|
||||
afterTimeType: string;
|
||||
};
|
||||
parent_id: null | string;
|
||||
preheader: string;
|
||||
queue: boolean;
|
||||
queue: Record<string, unknown>;
|
||||
reply_to_address: string;
|
||||
reply_to_name: string;
|
||||
segments: Array<unknown>;
|
||||
segments: Array<{ filters: unknown[] }>;
|
||||
sender_address: string;
|
||||
sender_name: string;
|
||||
sent_at: null | string;
|
||||
|
@ -13,7 +13,7 @@ import { Listings } from 'newsletters/automatic_emails/listings.jsx';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { NewsletterTypes } from 'newsletters/types';
|
||||
import { NewsletterTemplates } from 'newsletters/templates.jsx';
|
||||
import { NewsletterSend } from 'newsletters/send.jsx';
|
||||
import { NewsletterSend } from 'newsletters/send';
|
||||
import { Congratulate } from 'newsletters/send/congratulate/congratulate.jsx';
|
||||
import { NewsletterTypeStandard } from 'newsletters/types/standard.jsx';
|
||||
import { NewsletterNotification } from 'newsletters/types/notification/notification.jsx';
|
||||
|
@ -1,10 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
import { Component } from 'react';
|
||||
import { ChangeEvent, Component } from 'react';
|
||||
import jQuery from 'jquery';
|
||||
import PropTypes from 'prop-types';
|
||||
import { History, Location } from 'history';
|
||||
import ReactStringReplace from 'react-string-replace';
|
||||
import slugify from 'slugify';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { match as RouterMatch, withRouter } from 'react-router-dom';
|
||||
|
||||
import { Background } from 'common/background/background';
|
||||
import { Button, ErrorBoundary } from 'common';
|
||||
@ -18,27 +18,47 @@ import { WelcomeNewsletterFields } from 'newsletters/send/welcome.jsx';
|
||||
import { AutomaticEmailFields } from 'newsletters/send/automatic.jsx';
|
||||
import { ReEngagementNewsletterFields } from 'newsletters/send/re_engagement';
|
||||
import { Tooltip } from 'help-tooltip.jsx';
|
||||
import { fromUrl } from 'common/thumbnail.ts';
|
||||
|
||||
import { fromUrl } from 'common/thumbnail';
|
||||
import { GlobalContext } from 'context/index.jsx';
|
||||
|
||||
import { extractEmailDomain } from 'common/functions';
|
||||
import { mapFilterType } from '../analytics';
|
||||
import { PremiumModal } from '../common/premium_modal';
|
||||
import { NewsLetter, NewsletterType } from './models';
|
||||
import { PendingNewsletterMessage } from './send/pending_newsletter_message';
|
||||
|
||||
const automaticEmails = window.mailpoet_woocommerce_automatic_emails || [];
|
||||
const automaticEmails = window.mailpoet_woocommerce_automatic_emails || {};
|
||||
|
||||
const generateGaTrackingCampaignName = (id, subject) => {
|
||||
const generateGaTrackingCampaignName = (
|
||||
id: NewsLetter['id'],
|
||||
subject: NewsLetter['subject'],
|
||||
): string => {
|
||||
const name = slugify(subject, { strict: true, lower: true });
|
||||
return `${name || 'email'}-${id}`;
|
||||
};
|
||||
|
||||
const getTimingValueForTracking = (emailOpts) =>
|
||||
type NewsletterSendComponentProps = {
|
||||
match: RouterMatch<{
|
||||
id: string;
|
||||
}>;
|
||||
history: History;
|
||||
location: Location;
|
||||
};
|
||||
type NewsletterSendComponentState = {
|
||||
fields: Record<string, unknown>[] | boolean;
|
||||
item: NewsLetter;
|
||||
loading: boolean;
|
||||
thumbnailPromise?: Promise<unknown>;
|
||||
showPremiumModal: boolean;
|
||||
validationError?: string | JSX.Element;
|
||||
mssKeyPendingApproval: boolean;
|
||||
};
|
||||
const getTimingValueForTracking = (emailOpts: NewsLetter['options']) =>
|
||||
emailOpts.afterTimeType === 'immediate'
|
||||
? 'immediate'
|
||||
: `${emailOpts.afterTimeNumber} ${emailOpts.afterTimeType}`;
|
||||
|
||||
function validateNewsletter(newsletter) {
|
||||
function validateNewsletter(newsletter: NewsLetter) {
|
||||
let body;
|
||||
let content;
|
||||
|
||||
@ -106,20 +126,25 @@ function validateNewsletter(newsletter) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
class NewsletterSendComponent extends Component {
|
||||
constructor(props) {
|
||||
class NewsletterSendComponent extends Component<
|
||||
NewsletterSendComponentProps,
|
||||
NewsletterSendComponentState
|
||||
> {
|
||||
constructor(props: Readonly<NewsletterSendComponentProps>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fields: [],
|
||||
item: {},
|
||||
item: {} as NewsLetter,
|
||||
loading: true,
|
||||
thumbnailPromise: null,
|
||||
showPremiumModal: false,
|
||||
mssKeyPendingApproval: window.mailpoet_mss_key_pending_approval,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadItem(this.props.match.params.id).always(() => {
|
||||
// safe to ignore since even on rejection the state is updated
|
||||
void this.loadItem(this.props.match.params.id).always(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
jQuery('#mailpoet_newsletter').parsley({
|
||||
@ -129,13 +154,14 @@ class NewsletterSendComponent extends Component {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.match.params.id !== prevProps.match.params.id) {
|
||||
this.loadItem(this.props.match.params.id).always(() => {
|
||||
// safe to ignore since even on rejection the state is updated
|
||||
void this.loadItem(this.props.match.params.id).always(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getFieldsByNewsletter = (newsletter) => {
|
||||
getFieldsByNewsletter = (newsletter: NewsLetter) => {
|
||||
const type = this.getSubtype(newsletter);
|
||||
return type.getFields(newsletter);
|
||||
};
|
||||
@ -145,7 +171,14 @@ class NewsletterSendComponent extends Component {
|
||||
return type.getSendButtonOptions(this.state.item);
|
||||
};
|
||||
|
||||
getSubtype = (newsletter) => {
|
||||
getSubtype = (newsletter: NewsLetter) => {
|
||||
if (
|
||||
newsletter.type === NewsletterType.Automatic &&
|
||||
automaticEmails[newsletter.options.group]
|
||||
) {
|
||||
return AutomaticEmailFields;
|
||||
}
|
||||
|
||||
switch (newsletter.type) {
|
||||
case 'notification':
|
||||
return NotificationNewsletterFields;
|
||||
@ -153,18 +186,13 @@ class NewsletterSendComponent extends Component {
|
||||
return WelcomeNewsletterFields;
|
||||
case 're_engagement':
|
||||
return ReEngagementNewsletterFields;
|
||||
case 'automatic':
|
||||
if (automaticEmails[newsletter.options.group]) {
|
||||
return AutomaticEmailFields;
|
||||
}
|
||||
// fall through
|
||||
default:
|
||||
return StandardNewsletterFields;
|
||||
}
|
||||
};
|
||||
|
||||
getThumbnailPromise = (url) =>
|
||||
this.state.thumbnailPromise ? this.state.thumbnailPromise : fromUrl(url);
|
||||
getThumbnailPromise = (url) => this.state?.thumbnailPromise ?? fromUrl(url);
|
||||
|
||||
isValid = () => jQuery('#mailpoet_newsletter').parsley().isValid();
|
||||
|
||||
@ -196,7 +224,7 @@ class NewsletterSendComponent extends Component {
|
||||
id,
|
||||
},
|
||||
})
|
||||
.done((response) => {
|
||||
.done((response: { data: NewsLetter; meta: { preview_url: string } }) => {
|
||||
const thumbnailPromise =
|
||||
response.data.status === 'draft'
|
||||
? this.getThumbnailPromise(response.meta.preview_url)
|
||||
@ -204,14 +232,14 @@ class NewsletterSendComponent extends Component {
|
||||
const item = response.data;
|
||||
// Automation type emails should redirect
|
||||
// to an associated automation from the send page
|
||||
if (item.type === 'automation') {
|
||||
if (item.type === NewsletterType.Automation) {
|
||||
const automationId = item.options?.automationId;
|
||||
const goToUrl = automationId
|
||||
? `admin.php?page=mailpoet-automation-editor&id=${automationId}`
|
||||
: '/new';
|
||||
return this.setState(
|
||||
{
|
||||
item: {},
|
||||
item: {} as NewsLetter,
|
||||
},
|
||||
() => {
|
||||
this.props.history.push(goToUrl);
|
||||
@ -235,7 +263,7 @@ class NewsletterSendComponent extends Component {
|
||||
.fail(() => {
|
||||
this.setState(
|
||||
{
|
||||
item: {},
|
||||
item: {} as NewsLetter,
|
||||
},
|
||||
() => {
|
||||
this.props.history.push('/new');
|
||||
@ -250,7 +278,7 @@ class NewsletterSendComponent extends Component {
|
||||
);
|
||||
thumbnailPromise
|
||||
.then((thumbnailData) => {
|
||||
MailPoet.Ajax.post({
|
||||
void MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'newsletterTemplates',
|
||||
action: 'save',
|
||||
@ -312,32 +340,35 @@ class NewsletterSendComponent extends Component {
|
||||
if (!valid) {
|
||||
// handling invalid error message is handled in sender_address_field component
|
||||
window.mailpoet_sender_address_field_blur();
|
||||
return MailPoet.Modal.loading(false);
|
||||
MailPoet.Modal.loading(false);
|
||||
} else {
|
||||
void this.saveNewsletter()
|
||||
.done(() => {
|
||||
this.setState({ loading: true });
|
||||
})
|
||||
.done((response) => {
|
||||
switch (response.data.type) {
|
||||
case 'notification':
|
||||
case 'welcome':
|
||||
case 'automatic':
|
||||
case 're_engagement':
|
||||
void this.activateNewsletter(response);
|
||||
break;
|
||||
default:
|
||||
void this.sendNewsletter(response);
|
||||
break;
|
||||
}
|
||||
})
|
||||
.fail((err) => {
|
||||
this.showError(err);
|
||||
this.setState({ loading: false });
|
||||
MailPoet.Modal.loading(false);
|
||||
});
|
||||
}
|
||||
return this.saveNewsletter(e)
|
||||
.done(() => {
|
||||
this.setState({ loading: true });
|
||||
})
|
||||
.done((response) => {
|
||||
switch (response.data.type) {
|
||||
case 'notification':
|
||||
case 'welcome':
|
||||
case 'automatic':
|
||||
case 're_engagement':
|
||||
return this.activateNewsletter(response);
|
||||
default:
|
||||
return this.sendNewsletter(response);
|
||||
}
|
||||
})
|
||||
.fail((err) => {
|
||||
this.showError(err);
|
||||
this.setState({ loading: false });
|
||||
MailPoet.Modal.loading(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
sendNewsletter = (newsletter) =>
|
||||
sendNewsletter = (saveResponse: { data: NewsLetter }) =>
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'sendingQueue',
|
||||
@ -348,7 +379,7 @@ class NewsletterSendComponent extends Component {
|
||||
})
|
||||
.done((response) => {
|
||||
// save template in recently sent category
|
||||
this.saveTemplate(newsletter, () => {
|
||||
this.saveTemplate(saveResponse, () => {
|
||||
if (window.mailpoet_show_congratulate_after_first_newsletter) {
|
||||
MailPoet.Modal.loading(false);
|
||||
this.props.history.push(`/send/congratulate/${this.state.item.id}`);
|
||||
@ -358,7 +389,7 @@ class NewsletterSendComponent extends Component {
|
||||
this.props.history.push(`/${this.state.item.type || ''}`);
|
||||
// prepare segments
|
||||
let filters = [];
|
||||
newsletter.data.segments.map((segment) =>
|
||||
saveResponse.data.segments.map((segment) =>
|
||||
filters.push(...segment.filters),
|
||||
);
|
||||
filters = _.uniqWith(
|
||||
@ -397,7 +428,7 @@ class NewsletterSendComponent extends Component {
|
||||
MailPoet.Modal.loading(false);
|
||||
});
|
||||
|
||||
activateNewsletter = (newsletter) =>
|
||||
activateNewsletter = (saveResponse: { data: NewsLetter }) =>
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'newsletters',
|
||||
@ -409,7 +440,7 @@ class NewsletterSendComponent extends Component {
|
||||
})
|
||||
.done((response) => {
|
||||
// save template in recently sent category
|
||||
this.saveTemplate(newsletter, () => {
|
||||
this.saveTemplate(saveResponse, () => {
|
||||
if (window.mailpoet_show_congratulate_after_first_newsletter) {
|
||||
MailPoet.Modal.loading(false);
|
||||
this.props.history.push(`/send/congratulate/${this.state.item.id}`);
|
||||
@ -431,7 +462,7 @@ class NewsletterSendComponent extends Component {
|
||||
<p>
|
||||
{MailPoet.I18n.t('automaticEmailActivated').replace(
|
||||
'%1s',
|
||||
automaticEmails[opts.group].title,
|
||||
automaticEmails[opts.group]?.title ?? '',
|
||||
)}
|
||||
</p>,
|
||||
);
|
||||
@ -476,12 +507,12 @@ class NewsletterSendComponent extends Component {
|
||||
if (!this.isValid()) {
|
||||
jQuery('#mailpoet_newsletter').parsley().validate();
|
||||
} else {
|
||||
this.saveNewsletter(e)
|
||||
void this.saveNewsletter()
|
||||
.done(() => {
|
||||
this.setState({ loading: true });
|
||||
})
|
||||
.done(() => {
|
||||
MailPoet.Ajax.post({
|
||||
void MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'sendingQueue',
|
||||
action: 'resume',
|
||||
@ -512,7 +543,7 @@ class NewsletterSendComponent extends Component {
|
||||
handleSave = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
this.saveNewsletter(e)
|
||||
void this.saveNewsletter()
|
||||
.done(() => {
|
||||
this.context.notices.success(
|
||||
<p>{MailPoet.I18n.t('newsletterUpdated')}</p>,
|
||||
@ -534,7 +565,7 @@ class NewsletterSendComponent extends Component {
|
||||
e.preventDefault();
|
||||
const redirectTo = e.target.href;
|
||||
|
||||
this.saveNewsletter(e)
|
||||
void this.saveNewsletter()
|
||||
.done(() => {
|
||||
this.context.notices.success(
|
||||
<p>{MailPoet.I18n.t('newsletterUpdated')}</p>,
|
||||
@ -565,7 +596,7 @@ class NewsletterSendComponent extends Component {
|
||||
];
|
||||
const newsletterData = _.omit(data, IGNORED_NEWSLETTER_PROPERTIES);
|
||||
|
||||
return MailPoet.Ajax.post({
|
||||
return MailPoet.Ajax.post<{ data: NewsLetter }>({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'newsletters',
|
||||
action: 'save',
|
||||
@ -587,10 +618,10 @@ class NewsletterSendComponent extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
handleFormChange = (e) => {
|
||||
handleFormChange = (e: ChangeEvent<HTMLFormElement & { value: string }>) => {
|
||||
const name = e.target.name;
|
||||
const value = e.target.value;
|
||||
this.setState((prevState) => {
|
||||
this.setState((prevState: NewsletterSendComponentState) => {
|
||||
const item = prevState.item;
|
||||
const oldSubject = item.subject;
|
||||
const oldGaCampaign = item.ga_campaign;
|
||||
@ -646,17 +677,27 @@ class NewsletterSendComponent extends Component {
|
||||
return field;
|
||||
};
|
||||
|
||||
getPreparedFields = (isPaused, gaFieldDisabled) =>
|
||||
this.state.fields
|
||||
getPreparedFields = (isPaused, gaFieldDisabled) => {
|
||||
if (!Array.isArray(this.state.fields)) {
|
||||
return [];
|
||||
}
|
||||
return this.state.fields
|
||||
.map(this.disableSegmentsSelectorWhenPaused(isPaused))
|
||||
.map(this.disableGAIfPremiumInactive(gaFieldDisabled));
|
||||
};
|
||||
|
||||
closePremiumModal = () => this.setState({ showPremiumModal: false });
|
||||
|
||||
toggleLoadingState = (loading: boolean): void => this.setState({ loading });
|
||||
|
||||
updatePendingApprovalState = (mssKeyPendingApproval: boolean): void =>
|
||||
this.setState({ mssKeyPendingApproval });
|
||||
|
||||
render() {
|
||||
const {
|
||||
showPremiumModal,
|
||||
item: { status, queue, type, options },
|
||||
mssKeyPendingApproval,
|
||||
} = this.state;
|
||||
const isPaused = status === 'sending' && queue && queue.status === 'paused';
|
||||
const sendButtonOptions = this.getSendButtonOptions();
|
||||
@ -664,12 +705,12 @@ class NewsletterSendComponent extends Component {
|
||||
|
||||
const sendingDisabled = !!(
|
||||
window.mailpoet_subscribers_limit_reached ||
|
||||
window.mailpoet_mss_key_pending_approval ||
|
||||
mssKeyPendingApproval ||
|
||||
this.state.validationError !== undefined
|
||||
);
|
||||
|
||||
let emailType = type;
|
||||
if (emailType === 'automatic') {
|
||||
let emailType: string = type;
|
||||
if (emailType === NewsletterType.Automatic) {
|
||||
emailType = options.group || emailType;
|
||||
}
|
||||
|
||||
@ -739,23 +780,12 @@ class NewsletterSendComponent extends Component {
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
{window.mailpoet_mss_key_pending_approval && (
|
||||
<div className="mailpoet_error">
|
||||
{ReactStringReplace(
|
||||
MailPoet.I18n.t('pendingKeyApprovalNotice'),
|
||||
/\[link\](.*?)\[\/link\]/g,
|
||||
(match) => (
|
||||
<a
|
||||
key="pendingKeyApprovalNoticeLink"
|
||||
href="https://account.mailpoet.com/authorization"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{match}
|
||||
</a>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
{mssKeyPendingApproval && (
|
||||
<PendingNewsletterMessage
|
||||
toggleLoadingState={this.toggleLoadingState}
|
||||
updatePendingState={this.updatePendingApprovalState}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showPremiumModal && (
|
||||
@ -771,18 +801,4 @@ class NewsletterSendComponent extends Component {
|
||||
}
|
||||
|
||||
NewsletterSendComponent.contextType = GlobalContext;
|
||||
|
||||
NewsletterSendComponent.propTypes = {
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
NewsletterSendComponent.displayName = 'NewsletterSend';
|
||||
|
||||
export const NewsletterSend = withRouter(NewsletterSendComponent);
|
@ -1,12 +1,19 @@
|
||||
import { useState } from 'react';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
|
||||
import { Heading } from 'common/typography/heading/heading';
|
||||
import { WelcomeWizardStepLayoutBody } from '../../../wizard/layout/step_layout_body.jsx';
|
||||
import {
|
||||
Controls,
|
||||
FreeBenefitsList,
|
||||
} from '../../../wizard/steps/pitch_mss_step.jsx';
|
||||
import { WelcomeWizardStepLayoutBody } from 'wizard/layout/step_layout_body.jsx';
|
||||
import { Button, Heading, List } from 'common';
|
||||
|
||||
function FreeBenefitsList(): JSX.Element {
|
||||
return (
|
||||
<List>
|
||||
<li>{MailPoet.I18n.t('congratulationsMSSPitchList1')}</li>
|
||||
<li>{MailPoet.I18n.t('congratulationsMSSPitchList2')}</li>
|
||||
<li>{MailPoet.I18n.t('congratulationsMSSPitchList3')}</li>
|
||||
<li>{MailPoet.I18n.t('congratulationsMSSPitchList4')}</li>
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
MSSPitchIllustrationUrl: string;
|
||||
@ -35,6 +42,11 @@ function getHeader(newsletterType: string): string {
|
||||
|
||||
export function PitchMss(props: Props): JSX.Element {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const next = (): void => {
|
||||
props.onFinish();
|
||||
setIsClosing(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading level={1}>{getHeader(props.newsletter.type)}</Heading>
|
||||
@ -48,23 +60,48 @@ export function PitchMss(props: Props): JSX.Element {
|
||||
<p>
|
||||
{MailPoet.I18n.t(
|
||||
props.subscribersCount < 1000
|
||||
? 'welcomeWizardMSSFreeSubtitle'
|
||||
: 'welcomeWizardMSSNotFreeSubtitle',
|
||||
? 'congratulationsMSSPitchFreeSubtitle'
|
||||
: 'congratulationsMSSPitchNotFreeSubtitle',
|
||||
)}
|
||||
</p>
|
||||
<Heading level={5}>
|
||||
{MailPoet.I18n.t('welcomeWizardMSSFreeListTitle')}:
|
||||
{MailPoet.I18n.t('congratulationsMSSPitchFreeListTitle')}:
|
||||
</Heading>
|
||||
<FreeBenefitsList />
|
||||
<Controls
|
||||
mailpoetAccountUrl={props.purchaseUrl}
|
||||
next={(): void => {
|
||||
props.onFinish();
|
||||
setIsClosing(true);
|
||||
|
||||
<div className="mailpoet-gap" />
|
||||
<div className="mailpoet-gap" />
|
||||
|
||||
<Button
|
||||
isFullWidth
|
||||
href={props.purchaseUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
window.open(props.purchaseUrl);
|
||||
next();
|
||||
}}
|
||||
nextButtonText={MailPoet.I18n.t('welcomeWizardMSSFreeButton')}
|
||||
nextWithSpinner={isClosing}
|
||||
/>
|
||||
>
|
||||
{MailPoet.I18n.t('congratulationsMSSPitchFreeButton')}
|
||||
</Button>
|
||||
<Button
|
||||
isFullWidth
|
||||
variant="tertiary"
|
||||
onClick={next}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
['keydown', 'keypress'].includes(event.type) &&
|
||||
['Enter', ' '].includes(event.key)
|
||||
) {
|
||||
event.preventDefault();
|
||||
next();
|
||||
}
|
||||
}}
|
||||
withSpinner={isClosing}
|
||||
>
|
||||
{MailPoet.I18n.t('congratulationsMSSPitchNoThanks')}
|
||||
</Button>
|
||||
</div>
|
||||
</WelcomeWizardStepLayoutBody>
|
||||
</>
|
||||
|
@ -0,0 +1,76 @@
|
||||
import { MouseEvent, useCallback, useState } from 'react';
|
||||
import ReactStringReplace from 'react-string-replace';
|
||||
import { callApi, getLinkRegex, isTruthy, t, withBoundary } from 'common';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
|
||||
function PendingNewsletterMessage({
|
||||
toggleLoadingState,
|
||||
updatePendingState,
|
||||
}: {
|
||||
toggleLoadingState: (loading: boolean) => void;
|
||||
updatePendingState: (isPending: boolean) => void;
|
||||
}) {
|
||||
const refreshMssKeyState = useCallback(async () => {
|
||||
try {
|
||||
const { success, res } = await callApi<{
|
||||
result: { data: { is_approved: boolean | string | number } };
|
||||
}>({
|
||||
endpoint: 'services',
|
||||
action: 'refreshMSSKeyStatus',
|
||||
});
|
||||
|
||||
if (success === true) {
|
||||
updatePendingState(!isTruthy(res.data.result.data.is_approved));
|
||||
} else {
|
||||
MailPoet.Notice.showApiErrorNotice(res);
|
||||
}
|
||||
} catch (error) {
|
||||
MailPoet.Notice.showApiErrorNotice(error);
|
||||
}
|
||||
}, [updatePendingState]);
|
||||
|
||||
const [showRefreshButton, keepShowingRefresh] = useState(true);
|
||||
|
||||
const recheckKey = async (e: MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
toggleLoadingState(true);
|
||||
await refreshMssKeyState();
|
||||
keepShowingRefresh(false);
|
||||
toggleLoadingState(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mailpoet_error">
|
||||
{ReactStringReplace(
|
||||
t('pendingKeyApprovalNotice'),
|
||||
getLinkRegex(),
|
||||
(match) => (
|
||||
<a
|
||||
href="https://account.mailpoet.com/authorization"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{match}
|
||||
</a>
|
||||
),
|
||||
)}{' '}
|
||||
{showRefreshButton &&
|
||||
ReactStringReplace(
|
||||
t('pendingKeyApprovalNoticeRefresh'),
|
||||
getLinkRegex(),
|
||||
(match) => (
|
||||
<a href="#" onClick={recheckKey}>
|
||||
{match}
|
||||
</a>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PendingNewsletterMessage.displayName = 'PendingNewsletterMessage';
|
||||
|
||||
const PendingNewsletterMessageWithBoundary = withBoundary(
|
||||
PendingNewsletterMessage,
|
||||
);
|
||||
export { PendingNewsletterMessageWithBoundary as PendingNewsletterMessage };
|
@ -20,15 +20,6 @@ interface Props {
|
||||
hideClosingButton?: boolean;
|
||||
}
|
||||
|
||||
interface NewsletterTypesWindow extends Window {
|
||||
mailpoet_woocommerce_transactional_email_id: string;
|
||||
mailpoet_is_new_user: boolean;
|
||||
mailpoet_editor_javascript_url: string;
|
||||
mailpoet_woocommerce_automatic_emails: Record<string, unknown>;
|
||||
}
|
||||
|
||||
declare let window: NewsletterTypesWindow;
|
||||
|
||||
function NewsletterTypesComponent({
|
||||
filter,
|
||||
history,
|
||||
|
@ -74,9 +74,9 @@ jQuery(($) => {
|
||||
}
|
||||
const audioCaptchaSource = audioCaptcha.querySelector('source');
|
||||
let captchaSrc = captcha.getAttribute('src');
|
||||
let hashPos = captchaSrc.indexOf('#');
|
||||
let hashPos = captchaSrc.indexOf('&cachebust=');
|
||||
let newSrc = hashPos > 0 ? captchaSrc.substring(0, hashPos) : captchaSrc;
|
||||
captcha.setAttribute('src', `${newSrc}#${new Date().getTime()}`);
|
||||
captcha.setAttribute('src', `${newSrc}&cachebust=${new Date().getTime()}`);
|
||||
|
||||
captchaSrc = audioCaptchaSource.getAttribute('src');
|
||||
hashPos = captchaSrc.indexOf('&cachebust=');
|
||||
|
@ -1,86 +1,11 @@
|
||||
import { useContext } from 'react';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { STORE_NAME } from 'settings/store/store_name';
|
||||
import { select } from '@wordpress/data';
|
||||
import { useAction, useSelector, useSetting } from 'settings/store/hooks';
|
||||
import { GlobalContext } from 'context';
|
||||
import { Button } from 'common/button/button';
|
||||
import { t } from 'common/functions';
|
||||
import { Input } from 'common/form/input/input';
|
||||
import { KeyActivationState, MssStatus } from 'settings/store/types';
|
||||
import { MssStatus } from 'settings/store/types';
|
||||
import { Inputs, Label } from 'settings/components';
|
||||
import { SetFromAddressModal } from 'common/set_from_address_modal';
|
||||
import ReactStringReplace from 'react-string-replace';
|
||||
import {
|
||||
KeyMessages,
|
||||
MssMessages,
|
||||
PremiumMessages,
|
||||
ServiceUnavailableMessage,
|
||||
} from './messages';
|
||||
|
||||
type KeyState = {
|
||||
is_approved: boolean;
|
||||
};
|
||||
|
||||
function Messages(
|
||||
state: KeyActivationState,
|
||||
showPendingApprovalNotice: boolean,
|
||||
activationCallback: () => Promise<void>,
|
||||
) {
|
||||
if (state.code === 503) {
|
||||
return (
|
||||
<div className="key-activation-messages">
|
||||
<ServiceUnavailableMessage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="key-activation-messages">
|
||||
<KeyMessages />
|
||||
{state.mssStatus !== null && (
|
||||
<MssMessages
|
||||
keyMessage={state.mssMessage}
|
||||
activationCallback={activationCallback}
|
||||
/>
|
||||
)}
|
||||
{state.congratulatoryMssEmailSentTo && (
|
||||
<div className="mailpoet_success_item mailpoet_success">
|
||||
{t('premiumTabCongratulatoryMssEmailSent').replace(
|
||||
'[email_address]',
|
||||
state.congratulatoryMssEmailSentTo,
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{state.premiumStatus !== null && (
|
||||
<PremiumMessages keyMessage={state.premiumMessage} />
|
||||
)}
|
||||
|
||||
{showPendingApprovalNotice && (
|
||||
<div className="mailpoet_success">
|
||||
<div className="pending_approval_heading">
|
||||
{t('premiumTabPendingApprovalHeading')}
|
||||
</div>
|
||||
<div>{t('premiumTabPendingApprovalMessage')}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!state.isKeyValid && (
|
||||
<p>
|
||||
<a
|
||||
href="https://kb.mailpoet.com/article/319-known-errors-when-validating-a-mailpoet-key"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-beacon-article="5ef1da9d2c7d3a10cba966c5"
|
||||
className="mailpoet_error"
|
||||
>
|
||||
{MailPoet.I18n.t('learnMore')}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { KeyActivationButton } from 'common/premium_key/key_activation_button';
|
||||
import { KeyInput } from 'common/premium_key/key_input';
|
||||
|
||||
type Props = {
|
||||
subscribersCount: number;
|
||||
@ -115,18 +40,13 @@ const premiumTabGetKey = ReactStringReplace(
|
||||
);
|
||||
|
||||
export function KeyActivation({ subscribersCount }: Props) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { notices } = useContext<any>(GlobalContext);
|
||||
const state = useSelector('getKeyActivationState')();
|
||||
const setState = useAction('updateKeyActivationState');
|
||||
const verifyMssKey = useAction('verifyMssKey');
|
||||
const verifyPremiumKey = useAction('verifyPremiumKey');
|
||||
const sendCongratulatoryMssEmail = useAction('sendCongratulatoryMssEmail');
|
||||
const [senderAddress, setSenderAddress] = useSetting('sender', 'address');
|
||||
const [unauthorizedAddresses, setUnauthorizedAddresses] = useSetting(
|
||||
'authorized_emails_addresses_check',
|
||||
);
|
||||
const [apiKeyState] = useSetting('mta', 'mailpoet_api_key_state', 'data');
|
||||
const setSaveDone = useAction('setSaveDone');
|
||||
const setAuthorizedAddress = async (address: string) => {
|
||||
await setSenderAddress(address);
|
||||
@ -139,42 +59,6 @@ export function KeyActivation({ subscribersCount }: Props) {
|
||||
state.mssStatus === MssStatus.VALID_MSS_ACTIVE &&
|
||||
(!senderAddress || unauthorizedAddresses);
|
||||
|
||||
const showPendingApprovalNotice =
|
||||
state.inProgress === false &&
|
||||
state.mssStatus === MssStatus.VALID_MSS_ACTIVE &&
|
||||
apiKeyState &&
|
||||
(apiKeyState as KeyState).is_approved === false;
|
||||
|
||||
const verifyKey = async () => {
|
||||
if (!state.key) {
|
||||
notices.error(<p>{t('premiumTabNoKeyNotice')}</p>, { scroll: true });
|
||||
return;
|
||||
}
|
||||
await setState({
|
||||
mssStatus: null,
|
||||
premiumStatus: null,
|
||||
premiumInstallationStatus: null,
|
||||
});
|
||||
MailPoet.Modal.loading(true);
|
||||
setState({ inProgress: true });
|
||||
await verifyMssKey(state.key);
|
||||
const currentMssStatus =
|
||||
select(STORE_NAME).getKeyActivationState().mssStatus;
|
||||
if (currentMssStatus === MssStatus.VALID_MSS_ACTIVE) {
|
||||
await sendCongratulatoryMssEmail();
|
||||
}
|
||||
await verifyPremiumKey(state.key);
|
||||
setState({ inProgress: false });
|
||||
MailPoet.Modal.loading(false);
|
||||
setState({ fromAddressModalCanBeShown: true });
|
||||
};
|
||||
|
||||
async function activationCallback() {
|
||||
await verifyMssKey(state.key);
|
||||
sendCongratulatoryMssEmail();
|
||||
setState({ fromAddressModalCanBeShown: true });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mailpoet-settings-grid">
|
||||
<Label
|
||||
@ -205,25 +89,8 @@ export function KeyActivation({ subscribersCount }: Props) {
|
||||
}
|
||||
/>
|
||||
<Inputs>
|
||||
<Input
|
||||
type="text"
|
||||
id="mailpoet_premium_key"
|
||||
name="premium[premium_key]"
|
||||
value={state.key || ''}
|
||||
onChange={(event) =>
|
||||
setState({
|
||||
mssStatus: null,
|
||||
premiumStatus: null,
|
||||
premiumInstallationStatus: null,
|
||||
key: event.target.value.trim() || null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button type="button" onClick={verifyKey}>
|
||||
{t('premiumTabVerifyButton')}
|
||||
</Button>
|
||||
{state.isKeyValid !== null &&
|
||||
Messages(state, showPendingApprovalNotice, activationCallback)}
|
||||
<KeyInput />
|
||||
<KeyActivationButton label={t('premiumTabVerifyButton')} />
|
||||
</Inputs>
|
||||
{showFromAddressModal && (
|
||||
<SetFromAddressModal
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { Notices } from 'notices/notices.jsx';
|
||||
import { Loading } from 'common/loading';
|
||||
import { t } from 'common/functions';
|
||||
@ -16,12 +15,6 @@ import {
|
||||
} from './pages';
|
||||
import { useSelector } from './store/hooks';
|
||||
|
||||
const trackTabSwitched = (tabKey: string) => {
|
||||
MailPoet.trackEvent('User has clicked a tab in Settings', {
|
||||
'Tab ID': tabKey,
|
||||
});
|
||||
};
|
||||
|
||||
export function Settings() {
|
||||
const isSaving = useSelector('isSaving')();
|
||||
const hasWooCommerce = useSelector('hasWooCommerce')();
|
||||
@ -31,10 +24,7 @@ export function Settings() {
|
||||
{isSaving && <Loading />}
|
||||
<Notices />
|
||||
<UnsavedChangesNotice storeName="mailpoet-settings" />
|
||||
<RoutedTabs
|
||||
activeKey="basics"
|
||||
onSwitch={(tabKey: string) => trackTabSwitched(tabKey)}
|
||||
>
|
||||
<RoutedTabs activeKey="basics">
|
||||
<Tab
|
||||
key="basics"
|
||||
title={t('basicsTab')}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { cloneDeep, set } from 'lodash';
|
||||
import { select } from '@wordpress/data';
|
||||
|
||||
import { MailPoet } from 'mailpoet';
|
||||
@ -19,6 +20,7 @@ export function* verifyMssKey(key: string) {
|
||||
action: 'checkMSSKey',
|
||||
data: { key },
|
||||
};
|
||||
|
||||
if (!success) {
|
||||
return updateKeyActivationState({
|
||||
mssStatus: MssStatus.INVALID,
|
||||
@ -34,9 +36,20 @@ export function* verifyMssKey(key: string) {
|
||||
return updateKeyActivationState(fields);
|
||||
}
|
||||
|
||||
const data = select(STORE_NAME).getSettings();
|
||||
const data = cloneDeep(select(STORE_NAME).getSettings());
|
||||
|
||||
data.mta_group = 'mailpoet';
|
||||
data.mta = { ...data.mta, method: 'MailPoet', mailpoet_api_key: key };
|
||||
data.mta = {
|
||||
...data.mta,
|
||||
method: 'MailPoet',
|
||||
mailpoet_api_key: key,
|
||||
};
|
||||
|
||||
data.mta = set(
|
||||
data.mta,
|
||||
'mailpoet_api_key_state.data.is_approved',
|
||||
res.data.result.data.is_approved,
|
||||
);
|
||||
data.signup_confirmation.enabled = '1';
|
||||
|
||||
const call = yield {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { t } from 'common/functions';
|
||||
import { t, isTruthy } from 'common';
|
||||
import { Settings } from './types';
|
||||
|
||||
function asString(defaultValue: string) {
|
||||
@ -50,7 +50,6 @@ 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');
|
||||
@ -159,7 +158,9 @@ export function normalizeSettings(data: Record<string, unknown>): Settings {
|
||||
],
|
||||
'check_error',
|
||||
),
|
||||
data: asIs,
|
||||
data: asObject({
|
||||
is_approved: isTruthy,
|
||||
}),
|
||||
code: asIs,
|
||||
}),
|
||||
}),
|
||||
|
@ -92,7 +92,15 @@ export type Settings = {
|
||||
| 'already_used'
|
||||
| 'check_error'
|
||||
| 'valid_underprivileged';
|
||||
data: Record<string, unknown>;
|
||||
data: Record<string, unknown> & {
|
||||
subscriber_limit: number;
|
||||
email_volume_limit: number;
|
||||
emails_sent: number;
|
||||
public_id: string;
|
||||
support_tier: string;
|
||||
site_active_subscriber_limit: number;
|
||||
is_approved: boolean;
|
||||
};
|
||||
code: number;
|
||||
};
|
||||
};
|
||||
@ -137,6 +145,7 @@ export type Settings = {
|
||||
newsletter_id: number | string;
|
||||
}>;
|
||||
};
|
||||
woocommerce_import_screen_displayed: '1' | '';
|
||||
};
|
||||
type Segment = {
|
||||
id: string;
|
||||
|
@ -11,7 +11,11 @@ export * as ReactTooltip from 'react-tooltip';
|
||||
export * as ReactStringReplace from 'react-string-replace';
|
||||
export * as Slugify from 'slugify';
|
||||
export { CheckboxControl as WordpressComponentsCheckboxControl } from '@wordpress/components';
|
||||
export { ExternalLink as WordpressComponentsExternalLink } from '@wordpress/components';
|
||||
export { RadioControl as WordpressComponentsRadioControl } from '@wordpress/components';
|
||||
export { SelectControl as WordpressComponentsSelectControl } from '@wordpress/components';
|
||||
export { TextControl as WordpressComponentsTextControl } from '@wordpress/components';
|
||||
export { TextareaControl as WordpressComponentsTextareaControl } from '@wordpress/components';
|
||||
export { __experimentalConfirmDialog as WordpressComponentsConfirmDialog } from '@wordpress/components';
|
||||
export { MenuItem as WordpressComponentsMenuItem } from '@wordpress/components';
|
||||
export { PanelBody as WordpressComponentsPanelBody } from '@wordpress/components';
|
||||
|
@ -19,3 +19,4 @@ import 'sending-paused-notices-fix-button.tsx'; // side effect - renders ReactDO
|
||||
import 'sending-paused-notices-resume-button.ts'; // side effect - executes on doc ready, adds events
|
||||
import 'sending-paused-notices-authorize-email.tsx'; // side effect - renders ReactDOM to document
|
||||
import 'landingpage/landingpage'; // side effect - renders ReactDOM to document
|
||||
import 'wizard/track_wizard_loaded_via_woocommerce.tsx';
|
||||
|
12
mailpoet/assets/js/src/wizard/finishWizard.ts
Normal file
12
mailpoet/assets/js/src/wizard/finishWizard.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { updateSettings } from './updateSettings';
|
||||
|
||||
export async function finishWizard(redirect_url = null) {
|
||||
await updateSettings({
|
||||
version: window.mailpoet_version,
|
||||
});
|
||||
if (redirect_url) {
|
||||
window.location.href = redirect_url;
|
||||
} else {
|
||||
window.location.href = window.finish_wizard_url;
|
||||
}
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { Button } from '../../common';
|
||||
import { Heading } from '../../common/typography/heading/heading';
|
||||
import { List } from '../../common/typography/list/list';
|
||||
|
||||
export function FreeBenefitsList() {
|
||||
return (
|
||||
<List>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSList1')}</li>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSList2')}</li>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSList4')}</li>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSList5')}</li>
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
export function NotFreeBenefitsList() {
|
||||
return (
|
||||
<List>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSNotFreeList1')}</li>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSNotFreeList2')}</li>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSNotFreeList3')}</li>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSNotFreeList4')}</li>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSNotFreeList5')}</li>
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
export function Controls(props) {
|
||||
return (
|
||||
<>
|
||||
<div className="mailpoet-gap" />
|
||||
<div className="mailpoet-gap" />
|
||||
|
||||
<Button
|
||||
isFullWidth
|
||||
href={props.mailpoetAccountUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
window.open(props.mailpoetAccountUrl);
|
||||
props.next();
|
||||
}}
|
||||
>
|
||||
{props.nextButtonText}
|
||||
</Button>
|
||||
<Button
|
||||
isFullWidth
|
||||
variant="tertiary"
|
||||
onClick={props.next}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
['keydown', 'keypress'].includes(event.type) &&
|
||||
['Enter', ' '].includes(event.key)
|
||||
) {
|
||||
event.preventDefault();
|
||||
props.next();
|
||||
}
|
||||
}}
|
||||
withSpinner={props.nextWithSpinner}
|
||||
>
|
||||
{MailPoet.I18n.t('welcomeWizardMSSNoThanks')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Controls.propTypes = {
|
||||
mailpoetAccountUrl: PropTypes.string.isRequired,
|
||||
next: PropTypes.func.isRequired,
|
||||
nextButtonText: PropTypes.string.isRequired,
|
||||
nextWithSpinner: PropTypes.bool,
|
||||
};
|
||||
|
||||
Controls.defaultProps = {
|
||||
nextWithSpinner: false,
|
||||
};
|
||||
|
||||
function FreePlanSubscribers(props) {
|
||||
return (
|
||||
<>
|
||||
<Heading level={1}>
|
||||
{MailPoet.I18n.t('welcomeWizardMSSFreeTitle')}
|
||||
</Heading>
|
||||
|
||||
<div className="mailpoet-gap" />
|
||||
<p>{MailPoet.I18n.t('welcomeWizardMSSFreeSubtitle')}</p>
|
||||
<div className="mailpoet-gap" />
|
||||
|
||||
<Heading level={5}>
|
||||
{MailPoet.I18n.t('welcomeWizardMSSFreeListTitle')}:
|
||||
</Heading>
|
||||
<FreeBenefitsList />
|
||||
|
||||
<Controls
|
||||
mailpoetAccountUrl={MailPoet.MailPoetComUrlFactory.getPurchasePlanUrl(
|
||||
MailPoet.subscribersCount,
|
||||
MailPoet.currentWpUserEmail,
|
||||
'starter',
|
||||
{ utm_medium: 'onboarding', utm_campaign: 'purchase' },
|
||||
)}
|
||||
next={props.next}
|
||||
nextButtonText={MailPoet.I18n.t('welcomeWizardMSSFreeButton')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
FreePlanSubscribers.propTypes = {
|
||||
next: PropTypes.func.isRequired,
|
||||
};
|
||||
FreePlanSubscribers.displayName = 'FreePlanSubscribers';
|
||||
|
||||
function NotFreePlanSubscribers(props) {
|
||||
return (
|
||||
<>
|
||||
<Heading level={1}>
|
||||
{MailPoet.I18n.t('welcomeWizardMSSNotFreeTitle')}
|
||||
</Heading>
|
||||
|
||||
<div className="mailpoet-gap" />
|
||||
<p>{MailPoet.I18n.t('welcomeWizardMSSNotFreeSubtitle')}:</p>
|
||||
<NotFreeBenefitsList />
|
||||
|
||||
<Controls
|
||||
mailpoetAccountUrl={props.mailpoetAccountUrl}
|
||||
next={props.next}
|
||||
nextButtonText={MailPoet.I18n.t('welcomeWizardMSSNotFreeButton')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
NotFreePlanSubscribers.propTypes = {
|
||||
mailpoetAccountUrl: PropTypes.string.isRequired,
|
||||
next: PropTypes.func.isRequired,
|
||||
};
|
||||
NotFreePlanSubscribers.displayName = 'NotFreePlanSubscribers';
|
||||
|
||||
function WelcomeWizardPitchMSSStep(props) {
|
||||
return props.subscribersCount < 1000 ? (
|
||||
<FreePlanSubscribers
|
||||
mailpoetAccountUrl={props.mailpoetAccountUrl}
|
||||
next={props.next}
|
||||
/>
|
||||
) : (
|
||||
<NotFreePlanSubscribers
|
||||
mailpoetAccountUrl={props.purchaseUrl}
|
||||
next={props.next}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
WelcomeWizardPitchMSSStep.propTypes = {
|
||||
next: PropTypes.func.isRequired,
|
||||
subscribersCount: PropTypes.number.isRequired,
|
||||
mailpoetAccountUrl: PropTypes.string.isRequired,
|
||||
purchaseUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export { WelcomeWizardPitchMSSStep };
|
34
mailpoet/assets/js/src/wizard/steps/pitch_mss_step.tsx
Normal file
34
mailpoet/assets/js/src/wizard/steps/pitch_mss_step.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import {
|
||||
useRouteMatch,
|
||||
Switch,
|
||||
Route,
|
||||
Redirect,
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
import { MSSStepFirstPart } from './pitch_mss_step/first_part';
|
||||
import { MSSStepSecondPart } from './pitch_mss_step/second_part';
|
||||
import { MSSStepThirdPart } from './pitch_mss_step/third_part';
|
||||
|
||||
function WelcomeWizardPitchMSSStep(): JSX.Element {
|
||||
const { path } = useRouteMatch();
|
||||
const { step } = useParams<{ step: string }>();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path={`${path}`}>
|
||||
<Redirect to={`/steps/${step}/part/1`} />
|
||||
</Route>
|
||||
<Route path={`${path}/part/1`}>
|
||||
<MSSStepFirstPart />
|
||||
</Route>
|
||||
<Route path={`${path}/part/2`}>
|
||||
<MSSStepSecondPart />
|
||||
</Route>
|
||||
<Route path={`${path}/part/3`}>
|
||||
<MSSStepThirdPart />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export { WelcomeWizardPitchMSSStep };
|
@ -0,0 +1,68 @@
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { external, Icon } from '@wordpress/icons';
|
||||
import { Heading } from 'common/typography/heading/heading';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { Button, List } from 'common';
|
||||
import { OwnEmailServiceNote } from './own_email_service_note';
|
||||
|
||||
const mailpoetAccountUrl =
|
||||
'https://account.mailpoet.com/?ref=plugin-wizard&utm_source=plugin&utm_medium=onboarding&utm_campaign=purchase';
|
||||
|
||||
function openMailPoetShopAndGoToTheNextPart(event, history, step: string) {
|
||||
event.preventDefault();
|
||||
window.open(mailpoetAccountUrl);
|
||||
history.push(`/steps/${step}/part/2`);
|
||||
}
|
||||
|
||||
function MSSStepFirstPart(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { step } = useParams<{ step: string }>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading level={1}>
|
||||
{MailPoet.I18n.t('welcomeWizardMSSFirstPartTitle')}
|
||||
</Heading>
|
||||
|
||||
<div className="mailpoet-gap" />
|
||||
<p>{MailPoet.I18n.t('welcomeWizardMSSFirstPartSubtitle')}</p>
|
||||
<div className="mailpoet-gap" />
|
||||
|
||||
<div className="mailpoet-welcome-wizard-mss-list">
|
||||
<List>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSList1')}</li>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSList2')}</li>
|
||||
{MailPoet.subscribersCount < 1000 ? (
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSList3Free')}</li>
|
||||
) : (
|
||||
<li>{MailPoet.I18n.t('welcomeWizardMSSList3Paid')}</li>
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
|
||||
<div className="mailpoet-gap" />
|
||||
<div className="mailpoet-gap" />
|
||||
|
||||
<Button
|
||||
className="mailpoet-wizard-continue-button"
|
||||
isFullWidth
|
||||
href={mailpoetAccountUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(event) =>
|
||||
openMailPoetShopAndGoToTheNextPart(event, history, step)
|
||||
}
|
||||
iconEnd={<Icon icon={external} />}
|
||||
>
|
||||
{MailPoet.I18n.t('welcomeWizardMSSFirstPartButton')}
|
||||
</Button>
|
||||
|
||||
<div className="mailpoet-gap" />
|
||||
<div className="mailpoet-gap" />
|
||||
|
||||
<OwnEmailServiceNote />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { MSSStepFirstPart };
|
@ -0,0 +1,67 @@
|
||||
import { useState } from '@wordpress/element';
|
||||
import { Modal } from '@wordpress/components';
|
||||
import ReactStringReplace from 'react-string-replace';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { Button } from 'common';
|
||||
import { finishWizard } from 'wizard/finishWizard';
|
||||
|
||||
function OwnEmailServiceNote(): JSX.Element {
|
||||
const [confirmationModalIsOpen, setConfirmationModalOpen] = useState(false);
|
||||
const openConfirmationModal = (e) => {
|
||||
e.preventDefault();
|
||||
setConfirmationModalOpen(true);
|
||||
};
|
||||
const closeConfirmationModal = () => setConfirmationModalOpen(false);
|
||||
const finishWithOwnService = async (e) => {
|
||||
e.preventDefault();
|
||||
await finishWizard('admin.php?page=mailpoet-settings#/mta/other');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{ReactStringReplace(
|
||||
MailPoet.I18n.t('welcomeWizardMSSAdvancedUsers'),
|
||||
/\[link](.*?)\[\/link]/g,
|
||||
(match, index) => (
|
||||
<a key={index} onClick={openConfirmationModal} href="#">
|
||||
{match}
|
||||
</a>
|
||||
),
|
||||
)}
|
||||
</p>
|
||||
{confirmationModalIsOpen && (
|
||||
<Modal
|
||||
className="mailpoet-welcome-wizard-confirmation-modal"
|
||||
title={MailPoet.I18n.t('welcomeWizardMSSConfirmationModalTitle')}
|
||||
onRequestClose={closeConfirmationModal}
|
||||
>
|
||||
<p>
|
||||
{MailPoet.mailFunctionEnabled
|
||||
? MailPoet.I18n.t(
|
||||
'welcomeWizardMSSConfirmationModalFirstParagraph',
|
||||
)
|
||||
: MailPoet.I18n.t(
|
||||
'welcomeWizardMSSConfirmationModalFirstParagraphWithoutMailFunction',
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{MailPoet.I18n.t(
|
||||
'welcomeWizardMSSConfirmationModalSecondParagraph',
|
||||
)}
|
||||
</p>
|
||||
<div className="mailpoet-welcome-wizard-confirmation-modal-buttons">
|
||||
<Button variant="secondary" onClick={closeConfirmationModal}>
|
||||
{MailPoet.I18n.t('welcomeWizardMSSConfirmationModalGoBackButton')}
|
||||
</Button>
|
||||
<Button onClick={finishWithOwnService}>
|
||||
{MailPoet.I18n.t('welcomeWizardMSSConfirmationModalOkButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { OwnEmailServiceNote };
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user