Compare commits

...

252 Commits
4.5.1 ... 4.7.0

Author SHA1 Message Date
30b7dfe603 Release 4.7.0 2023-02-20 14:53:54 +01:00
67455a0752 Bump WC requires at least to L - 2
[MAILPOET-4783]
2023-02-20 12:26:20 +01:00
7ca607f49f Update WC tested up to version
[MAILPOET-4783]
2023-02-20 12:26:20 +01:00
b3aff28335 Acceptance: wait for page elements to load before asserting
[MAILPOET-4783]
2023-02-20 12:26:20 +01:00
489fa9552b Acceptance: prioritize assertion with delay over immediate one
First checking for the delayed assertion and then running the
immediate one reduces the chance of flakiness

[MAILPOET-4783]
2023-02-20 12:26:20 +01:00
d49040a11e Accept: verify form is not loading when checking for select2
[MAILPOET-4783]
2023-02-20 12:26:20 +01:00
64f008e106 Acceptance: increase timeout for ever failing test
The increased timeout helped to get the test green on local

[MAILPOET-4783]
2023-02-20 12:26:20 +01:00
2f0d7137db Acceptance: wait for element to appear then try to clear it
[MAILPOET-4783]
2023-02-20 12:26:20 +01:00
68bd3314d9 Make automation acceptance test css selectors more specific
Less specific css selectors + css grid orders were making the
acceptance tests flaky

[MAILPOET-4783]
2023-02-20 12:26:20 +01:00
2083adc3d2 Make sure we call initializeMixpanelWhenLoaded when window.MailPoet is defined
The initializeMixpanelWhenLoaded expects window.MailPoet to be defined.
Instead of calling initializeMixpanelWhenLoaded as a side-effect of the import
we can export the function and make sure it is called after window.MailPoet is defined.
[MAILPOET-5065]
2023-02-20 11:34:09 +01:00
a4e8bf9a9c Update tests and add test for ab test button
MAILPOET-4972
2023-02-20 11:10:46 +01:00
7daa0c9dff Prevent redirecting on Experimental Page
MAILPOET-4972
2023-02-20 11:10:46 +01:00
027f3fbefe Setup ab test experiment debugger
MAILPOET-4972
2023-02-20 11:10:46 +01:00
e3c4728529 Save event data in storage pending when analytics permission is available.
We are reusing the `MailPoetTrackEvent` method because we overwrite the `MailPoet.trackEvent` method when mixpanel is loaded.

Unfortunately, mixpanel being available does not mean MailPoet has permission to track user events. We are saving the tracking data for when the permission becomes available.

MAILPOET-4972
2023-02-20 11:10:46 +01:00
06f231f4b1 Add an A/B test button component and setup basic events.
We are using the `@marvelapp/react-ab-test` package because the original `react-ab-test` package hasn't received an update in a while

MAILPOET-4972
2023-02-20 11:10:46 +01:00
4966d45d5e Update copy of the MailPoet link added to the WooCommerce task list
[MAILPOET-5063]
2023-02-19 11:24:52 +01:00
ae38774f85 Remove checking closed panel
For some reason, the click doesn't work properly when the mouse moves too fast away.
Because we don't check closing of the panel in another test I decided to remove it from this test too.
[MAILPOET-5060]
2023-02-17 07:36:42 +02:00
b907ca491b Fix TinyMCE prefixing
[MAILPOET-5052]
2023-02-16 10:50:47 +01:00
abfc33002c Make form close buttons focusable/usable with keyboard
[MAILPOET-4877]
2023-02-16 10:17:50 +01:00
94954c1601 Update Docker image for oldest acceptance tests to use WP 5.9
[MAILPOET-4893]
2023-02-15 13:21:45 +01:00
914b5752cd Upgrade minimum required WordPress version to 5.9
[MAILPOET-4893]
2023-02-15 13:21:45 +01:00
102da43d05 Add escaping of form name when printed in the old notification system
[MAILPOET-5056]
2023-02-15 12:55:02 +01:00
6cf9f7f1d6 Add link to form editor to the corrupted form data message
[MAILPOET-5056]
2023-02-15 12:55:02 +01:00
389017c3b9 Display an error notice when a form with malformed settings data is detected
[MAILPOET-5056]
2023-02-15 12:55:02 +01:00
0b4d18faa0 Prevent form listing crash when a form is missing settings data
[MAILPOET-5056]
2023-02-15 12:55:02 +01:00
623010a644 Use dataprovider and clean up early
[MAILPOET-4883]
2023-02-15 12:30:41 +01:00
9e62501c30 Update comment
[MAILPOET-4883]
2023-02-15 12:30:41 +01:00
95ec3d2fb9 Add integration test to ensure posts are probperly fetched
[MAILPEOT-4883]
2023-02-15 12:30:41 +01:00
f60ae7a8ef Set dynamic also correctly for products
[MAILPOET-4883]
2023-02-15 12:30:41 +01:00
eef47c5bfb Use small mixin breakpoint
[MAILPOET-4830]
2023-02-15 12:19:35 +01:00
a91108f52e Use more optimized image
[MAILPOET-4830]
2023-02-15 12:19:35 +01:00
f0772cc793 Add acceptance test for homepage upsell
[MAILPOET-4830]
2023-02-15 12:19:35 +01:00
fe20687ae8 Add batch method for creating subscribers
[MAILPOET-4830]
2023-02-15 12:19:35 +01:00
da5051f758 Add purchase URL to upsell
[MAILPOET-4830]
2023-02-15 12:19:35 +01:00
a9d8a2b164 Add condition for displaying
[MAILPOET-4830]
2023-02-15 12:19:35 +01:00
c6846a2d4c Make close button optional
[MAILPOET-4830]
2023-02-15 12:19:35 +01:00
81ffe6e4f9 Add styles for upsell component
[MAILPOET-4830]
2023-02-15 12:19:35 +01:00
ab3d27569f Make upsell component closable
[MAILPOET-4830]
2023-02-15 12:19:35 +01:00
337dcb603f Add homepage upsell component
[MAILPOET-4830]
2023-02-15 12:19:35 +01:00
084022010b Release 4.6.2 2023-02-14 15:15:17 +01:00
b83dac2ed9 Use setting types instead of inline types when setting tracking settings
[MAILPOET-5059]
2023-02-14 12:46:04 +01:00
ddf4c9109c Use the value of YesNo component directly as it is already boolean
[MAILPOET-5059]
2023-02-14 12:46:04 +01:00
40cdb9a766 Fix tracking settings configuration on WooCommerce setup wizard step
When testing the woo wizard step i noticed that always set tracking to
'partial' even if the checkbox is checked. When debugging I found that the allowed value
is not a string but boolean. I tested this in wizard and also on the WooCommerce setup page.
I needed to update the acceptance test because it was clicking on 'no' and asserting the outcome as 'yes' was clicked.
[MAILPOET-5059]
2023-02-14 12:46:04 +01:00
9cf70c6c3b Sync WooCommerce settings from Woo step in wizard to store
[MAILPOET-5059]
2023-02-14 12:46:04 +01:00
333b121b2a Update store when saving tracking preferences in wizard
[MAILPOET-5059]
2023-02-14 12:46:04 +01:00
1de328abaa Use settings store instead of local component state for sender data
In [MAILPOET-4818], we started using the settings Redux store to handle
adding the MSS key in the last step of the Welcome Wizard. This
introduced a bug in the previous steps as the same Redux store is not
used. Those steps rely on the component state to track changes to their
fields. This meant that once the user completed the last step, the
changes made in the previous steps were ignored and the default values
for the settings was saved to the database.

This commit fixes this bug for the first step of the wizard by using the
same settings Redux store to track changes to the sender name and
address.

[MAILPOET-5059]
2023-02-14 12:46:04 +01:00
6b4b1dfcbe Add basic acceptance test for the welcome wizard
[MAILPOET-5051]
2023-02-14 12:46:04 +01:00
66dc6e67ab Remove unnecessary override
[MAILPOET-4872]
2023-02-13 14:13:44 -03:00
d8d859d209 Declare properties to avoid dynamic usage in codeception
[MAILPOET-4872]
2023-02-13 14:13:44 -03:00
4bd7dd4ad6 Fix codeception PHP 8.2 deprecation notices
[MAILPOET-4872]
2023-02-13 14:13:44 -03:00
359f134881 Fix merge conflict after rebasing trunk
[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
a65866d2a8 Use refreshMSSKeyStatus in preview modal to refresh key status
[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
3917a559f7 Make a direct call to endpoint to update key pending status
[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
24eab9aac3 Refrain from initializing settings store on send page
[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
bb240d8e68 Improve call_api return typings
[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
f3bf4b36e9 Introduce endpoint to refresh mss key status
[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
d8729ef43c Show errors happening when refreshing mss key status
[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
4072daa91f Fix failing acceptance test due to change of string
[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
3ae281fc1f Add functionality to refresh mss key status in preview modal
[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
86ce19085a Fix failing test by ensuring mssKey is approved
If the mssKey is pending approval the activate button
of welcome newsletters stays disabled

[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
0aceab5a2e Add pending approval message and functionality to send page
Show a message to user about their pending approval and
allow them to update the status in place via ajax

[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
a48b725c7d Define pending newsletter message component
[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
cf5718122f Fix the issue with verifyMssKey overriding is_approved
The verifyMssKey side effect handler was overriding the value
of is_approved when updating the state.

[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
e7e4276bf2 Fix the issue with failing to set a truthy value correctly
is_approved was (sometimes) set to string false, which was
causing the js logic to fail

[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
551d68ff69 Convert sent.tsx to jsx
[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
61fa215607 Extend the pending approval message on settings/premium
The message is extended with a link to re-verify the key
and update the status

[MAILPOET-4633]
2023-02-13 16:56:54 +01:00
89df50c160 Always skip unknown migrations (not only completed ones)
[MAILPOET-5054]
2023-02-13 16:56:01 +01:00
e9326e8c9e Handle a case when migrations table contains invalid data
See: https://wordpress.org/support/topic/null-values-are-exported-as-empty-strings/

[MAILPOET-5054]
2023-02-13 16:56:01 +01:00
956d693454 Skip redirecting to MailPoet landing page when activated from WooCommerce setup wizard
MAILPOET-5058
2023-02-13 16:23:59 +01:00
a76fb6c63a Update form template "Relax" definition to display correctly
The popup and slide-in forms should not display email input and submit button in columns.
[MAILPOET-5047]
2023-02-13 12:14:05 +01:00
d061d451a4 Remove Export from Main menu options
MAILPOET-5045
2023-02-13 11:58:58 +01:00
52e19d445a Update pnpm to the latest version
[MAILPOET-5055]
2023-02-09 09:57:30 +02:00
c6a3e08c34 Fix soundasleep/html2text PHP 8.2 deprecation notices
[MAILPOET-4875]
2023-02-08 15:26:14 +01:00
a11a462eee Fix gregwar/captcha PHP 8.2 deprecation notices
[MAILPOET-4874]
2023-02-08 15:26:14 +01:00
310d689219 Ensure image reloads in safari
Safari does not reload the image when only the hash
changes. Therefore we use the cachebust get parameter
like we do already for the audio

[MAILPOET-5032]
2023-02-08 13:08:09 +01:00
118cc83cc2 Remove 'Click to refresh' in title because a click does no longer refresh
[MAILPOET-5032]
2023-02-08 13:08:09 +01:00
3da2144ead Extend audio type by range header
[MAILPOET-5032]
2023-02-08 13:08:09 +01:00
b17a9cb4ae Add test to ensure HTTP_RANGE headers do not reload captcha
[MAILPOET-5032]
2023-02-08 13:08:09 +01:00
351c8a0bd7 Remove the s and g parameters from the shop URL
Those parameters are not working as expected (see
https://github.com/mailpoet/mailpoet/pull/4650#issuecomment-1410881945
for more details). So we are removing them for now and they will be
added again on a separate ticket in the future
(https://mailpoet.atlassian.net/browse/MAILPOET-5046).

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
c705862e82 Use MailPoet module properties instead of the window object
We want to stop accessing window directly in JS code (see
https://mailpoet.atlassian.net/browse/MAILPOET-2943). So this commit
uses MailPoet module properties instead of accessing
window.mailpoet_mail_function_enabled and window.subscribers_count
directly.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
dd2f100acd Implement MSS key verification in the MSS step of the welcome wizard
[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
88398af343 Remove unused translatable strings
Back in 2020, commit bf13d08a22 removed the code that
used those strings but the strings themselves were never removed.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
28bc4d6943 Display the MSS step of the wizard when there is a MSS key set
Before we used to hide the MSS step of the welcome wizard when there was
already a key set. This commit stops this behavior and now we will
always display the MSS step as it will start allowing users to set the
MSS key in it.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
e1e690ad14 Refactor input and button to validate key to its own components
This commit refactors the <input> and <button> used to validate the MSS
key to its own components. After doing this, we will be able to reuse
this components outside of the settings app. In particular, we want to
be able to use it in the welcome wizard app.

It also implements one small change to the verify button that is part of
the changes related to the welcome wizard. Now the verify button is
disabled when the MSS key <input> is empty.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
110b67bd9c Refactor key activation messages to be reused in the welcome wizard
[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
8788e299e1 Extract updateSettings() and finishWizard to refrain from passing it down
[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
ed81ae1ccd Simplify code by accessing subscriber count directly
Instead of passing it down the component chain.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
d19e297d37 Refactor code to its own function outside component
Doing this to avoid redefining the function on each render and to
improve readability since a named function was used.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
c4ce4fd10b Simplify imports by adding an index to common/typography/
Doing this based on feedback from first draft version of the PR related
to this commit.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
527ebafc5c Use React Router to navigate between different parts of the MSS step
This replaces the first implementation that was just a temporary
implementation using the component state.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
e7f1325d3e Add tracking parameters to account.mailpoet.com links in the MSS step
[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
d7d3095824 Use icon for the list in the MSS step of the welcome wizard
It seems the <List> component doesn't accept a className parameter and
that is why I added a <div> around it.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
d9b103891b Fix style of the links in the MSS step of the welcome wizard
[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
fe8f90a4d8 Display OwnEmailServiceNote component on the second part of the MSS step
Before this component was displayed only on the first part of the MSS
step.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
6d7bece8d6 Implement functionality of the third part of the MSS step
Finish the wizard when the user clicks on the finish button.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
199f3e21e7 Implement temporary solution to move between the parts of this step
The solution will likely change in subsequent commits to use something
that chnages the URL as well so that users can reload the page when
displaying one of the parts.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
7e9caeffbf Implement design and content of the third part of the MSS step
This commit implements the design and content of the third part of the
MSS step in the welcome wizard. This is a new part of the final step.
The functionality is not yet implemented and will be in a subsequent
commit.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
a075b7bd68 Implement design and content of the second part of the MSS step
This commit implements the design and content of the second part of the
MSS step in the welcome wizard. This is a new part of the final step.
The functionality is not yet implemented and will be in a subsequent
commit.

The contents of the first part that already existed were moved to its
own file (first_part.tsx) as part of this commit as well.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
359f734024 Implement confirmation modal for setting up custom sending service
This commit implements the confirmation modal that is displayed when the
user clicks on the "I’ll set up my own email service" link in the MSS
step of the welcome wizard.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
a069ae6884 Update strings in the MSS step of the welcome wizard
This commit implements the changes to the strings of the MSS step of the
welcome wizard. The new functionality is still missing and will be
implemented in upcoming commits.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
6de8b0ceec Use different elements for success_pitch_mss.tsx and pitch_mss_step.tsx
Up until now success_pitch_mss.tsx and pitch_mss_step.tsx shared code
(mostly strings), but this will change in subsequent commits as part of
ticket MAILPOET-4818. So this commit copies shared code from
pitch_mss_step.tsx to success_pitch_mss.tsx. So that future changes in
one file we won't affect the other.

[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
edf34ccc2c Refactor pitch_mss_step.jsx to TSX
[MAILPOET-4818]
2023-02-08 12:56:32 +01:00
57f953933d Release 4.6.1 2023-02-07 09:19:18 -06:00
9b2e6208e0 Allow editors to manage automations
[MAILPOET-5024]
2023-02-06 14:06:06 +01:00
314327aed2 Add tests to ensure editor has access to endpoints
[MAILPOET-5024]
2023-02-06 14:06:06 +01:00
ce370c76a3 Fix cut-off placeholder in multi-select select2
[MAILPOET-4630]
2023-02-06 13:57:48 +01:00
153d29b167 Change height of select2 with single value to be the same as regular input
[MAILPOET-4630]
2023-02-06 13:57:48 +01:00
7858d4ada2 Link to the new homepage from the MailPoet task in WooCommerce
MAILPOET-4926
2023-02-06 13:38:33 +01:00
adc158367d Add wait and retry when translations API returns 429
[MAILPOET-4979]
2023-02-06 12:27:39 +01:00
69e67f3c7a Do not cache language packs response when code is not 200
[MAILPOET-4979]
2023-02-06 12:27:39 +01:00
4f831f8b17 Refactor API call for fetching language packs data to private method
[MAILPOET-4979]
2023-02-06 12:27:39 +01:00
36c95f80fd Add transient caching of language packs data
[MAILPOET-4979]
2023-02-06 12:27:39 +01:00
2fb36e57f9 Stop tracking "User has clicked a tab in Settings" event to Mixpanel
[MAILPOET-5031]
2023-02-06 12:22:06 +01:00
401501b97b Use @ts-expect-error
[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
d585c27f6e Convert map_form_data_before_saving.spec.js
[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
0775ce2669 Convert map_form_data_after_loading.spec.js
[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
37bb5fd824 Convert form_validator.spec.js
[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
fee5af2ffa Convert form_to_block_test_data.js
[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
21d03c5bf7 Convert form_body_to_blocks.spec.js
[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
0854b1ce36 Convert toggle_sidebar_panel.jsx
It also adds a new type for the action and converts the test.

[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
949c962fc9 Convert create_custom_field_started.jsx
It also adds a new type for the action and converts the test.

[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
17c0a76754 Rename Type
[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
d449f02883 Add functions to use partial types in tests
Functions to create mocks with Partial Types in tests and avoid using `as Type`. Also extract FormData type to form_data_types.ts

[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
b40c5a5dfe Convert history_record.ts.jsx to ts
Converts the file and test. It also moves HistorRecord type to state_types.ts

[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
08aea80a55 Convert save_form_started.jsx to ts
Converts the file and test. It also removes extra param from tests.

[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
86a4347908 Convert blocks_to_form_body.jsx to ts
Converts the file and tests. It also creates the type CustomFields.

[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
f242e847bb Convert selectors.spec.js to ts
I have decided to use `as State` on the mocks instead of creating a big object conforming to the type.
The functions tested only use one or two properties of the 33 the type has and I have verified the properties we use for the test are well-formed.

[MAILPOET-3523]
2023-02-06 12:19:21 +01:00
3c89bf2f0d Remove currency from the Woo Tracker data
The currency is already tracked by Woo core under settings.currency.
[MAILPOET-5037]
2023-02-06 12:13:27 +01:00
10ea5ec55b Remove extra sending method check for new users.
mta.method is probably not set yet.

MAILPOET-4929
2023-02-06 12:11:12 +01:00
805804f3d3 Add WP Filter to disable the mail function check
MAILPOET-4929
2023-02-06 12:11:12 +01:00
74549665d6 Stop checking for DisabledMailFunction when the plugin is repeatedly activated and updated.
We will only perform the check when the plugin is first activated and sending method is changed.

MAILPOET-4929
2023-02-06 12:11:12 +01:00
5b78ea9a69 Update test mail information for DisabledMailFunctionNotice
In this commit, we change the test mail subject, body and destination address

MAILPOET-4929
2023-02-06 12:11:12 +01:00
b25c8b8965 Do not use title attribute when alt text is not defined
[MAILPOET-4932]
2023-02-06 11:43:23 +01:00
da5b0ede16 Show the MailPoet logo on the landing page
This will show the topbar (including the MailPoet logo) and set content max-width

MAILPOET-5029
2023-02-06 10:59:27 +01:00
29de3e2bae Remove Promise from helper
[MAILPOET-4960]
2023-02-02 17:16:33 +01:00
79bc684312 Add new test Settings basics
[MAILPOET-4960]
2023-02-02 17:16:33 +01:00
7bc71429eb Add Copy to clipboard button
[MAILPOET-4526]
2023-02-01 12:48:35 +01:00
967988519b Fix deprecated callable usage in Sudzy library
[MAILPOET-4873]
2023-02-01 12:17:03 +01:00
3b5a96a3f7 Add test case that coupon is rendered correctly
[MAILPOET-4984]
2023-02-01 11:21:49 +01:00
83b14013ec Add coupon block unit test
[MAILPOET-4984]
2023-02-01 11:21:49 +01:00
cd9904de7d Fix using code instead of couponCode read by id
[MAILPOET-4984]
2023-02-01 11:21:49 +01:00
d305498613 Add coupon block acceptance test
[MAILPOET-4984]
2023-02-01 11:21:49 +01:00
69833557e4 Add newsletter editor test for coupon block
[MAILPOET-4984]
2023-02-01 11:21:49 +01:00
cd596245ce Add coupon default values
[MAILPOET-4984]
2023-02-01 11:21:49 +01:00
7d36d59e65 Allow rgb in the newsletter style attributes
[MAILPOET-4981]
2023-02-01 11:10:59 +01:00
efaa5073fb Query subscriber by user in shortcode
[MAILPOET-4184]
2023-02-01 10:53:26 +01:00
37f4082210 Remove wpUser authentication
[MAILPOET-4184]
2023-02-01 10:53:26 +01:00
f324abd1d0 Update woocommerce-stubs to version 7.3.0
[MAILPOET-4313]
2023-02-01 08:57:47 +02:00
5d56a0368a Update outdated PHP version message
Changing the message to make it more clear that PHP >= 7.2 and <= 7.3 is
not recommended but still supported.

[MAILPOET-4933]
2023-01-31 13:58:52 +01:00
106db48f8d Display the PHP outdated version warning for sites running PHP <= 7.3
[MAILPOET-4933]
2023-01-31 13:58:52 +01:00
ed2ecb7604 Release 4.6.0 2023-01-31 13:26:57 +01:00
10dcc4f45d Fix failing test due to a changed DMARC policy
It seems that the DMARC policy for automattic.com was changed from
"quarantine" to "reject".

[MAILPOET-5038]
2023-01-30 14:40:17 -03:00
3237351450 Move code to handle tracking source of wizard to its own file
Doing this based on feedback during the PR review: https://github.com/mailpoet/mailpoet/pull/4660/files#r1073420154

[MAILPOET-4814]
2023-01-30 15:01:42 +01:00
8d57e81b99 Use return type hint instead of docblock to tell PHP the return type
[MAILPOET-4814]
2023-01-30 15:01:42 +01:00
e37daa6c66 Delete setting instead of updating it when tracking wizard source
[MAILPOET-4814]
2023-01-30 15:01:42 +01:00
ad4247a241 Add API endpoint to delete a setting
[MAILPOET-4814]
2023-01-30 15:01:42 +01:00
8a4f5c13da Add tracking for users arriving to the wizard from WooCommerce
MailPoet adds a link to the WooCommerce task list pointing to its own
welcome wizard. We want to track users that arrive to the MP wizard from
WooCommerce but at this point tracking is not enabled. So we store the
information in a setting, and send the tracking event to Mixpanel, if it
is enabled, after the user completes the wizard.

[MAILPOET-4814]
2023-01-30 15:01:42 +01:00
1161e6f3f6 Add MailPoet task to WooCommerce homepage
[MAILPOET-4814]
2023-01-30 15:01:42 +01:00
e5d5e20efd Add escaping into rendering homepage_link shortcode
[MAILPOET-4936]
2023-01-30 14:39:35 +01:00
9fcb9afa9d Add site:homepage_url shortcode to helper
[MAILPOET-4936]
2023-01-30 14:39:35 +01:00
b24b7b86fd Update homepage link in default settings for confirmation email
[MAILPOET-4936]
2023-01-30 14:39:35 +01:00
ddca94891d Unify rendering site:homepate_link shortcode with value from helper
[MAILPOET-4936]
2023-01-30 14:39:35 +01:00
9f14f3cc08 Change inserted text in shortcode helper for Homepage link
[MAILPOET-4936]
2023-01-30 14:39:35 +01:00
6b3fc309cc Add new shortcode [site:homepage_url]
[MAILPOET-4936]
2023-01-30 14:39:35 +01:00
60d933b39a Fix capitalization of MSS
[MAILPOET-5030]
2023-01-30 14:25:41 +01:00
ea57fd9c11 Update pronouns in a string from "his/her" to "their"
[MAILPOET-5028]
2023-01-30 14:24:21 +01:00
a9788b04d4 Remove Code Sniffer ignore
[MAILPOET-4900]
2023-01-30 14:13:33 +01:00
46c45d9bef Fix consolidation/robo PHP 8.2 deprecation notices
[MAILPOET-4900]

When a new robo version with a fix for 'self' in callables is released,
this patch can be removed. Check here:
https://github.com/consolidation/robo/issues/1135
2023-01-30 14:13:33 +01:00
3282e2f063 Update robo and its dependency to PHP 8.2 compatible versions
[MAILPOET-4900]
2023-01-30 14:13:33 +01:00
05e941e449 Allow dynamic genration of coupon code for some newsletters
Those newsletters that allow updating their body html should
persist the couponId along side the Coupon block settings so
that on the next rendering attempt the same coupon code is
used

[MAILPOET-4763]
2023-01-30 12:46:38 +01:00
6a14a3f7b1 Stop saving invalid input values
MAILPOET-4762
2023-01-30 11:48:48 +01:00
b858f1159a Update tests and remove unused variables
MAILPOET-4762
2023-01-30 11:48:48 +01:00
f3d73aae03 Escape the content for placeholder
MAILPOET-4762
2023-01-30 11:48:48 +01:00
494723c818 Use WooCommerce decimal separators when validating input fields
We need to use the decimal separators selected by the user for our validation. We are also using the validation used on the adding/editing coupon page

MAILPOET-4762
2023-01-30 11:48:48 +01:00
93af9d491d Add validation for some fields
MAILPOET-4762
2023-01-30 11:48:48 +01:00
b07d34ee23 Fix tests
MAILPOET-4762
2023-01-30 11:48:48 +01:00
4b2b94db9e Add Products and Exclude products Usage restriction fields
MAILPOET-4762
2023-01-30 11:48:48 +01:00
77b9cea62c Add Product categories and Exclude categories Usage restriction fields
MAILPOET-4762
2023-01-30 11:48:48 +01:00
e2dc137b59 Add Usage limit fields and half of Usage restriction fields
MAILPOET-4762
2023-01-30 11:48:48 +01:00
2da4f5f3b9 Add basic accordion to group coupon data and free shipping field
MAILPOET-4762
2023-01-30 11:48:48 +01:00
7549ed7f0f Add other WooCommerce coupon option methods
MAILPOET-4762
2023-01-30 11:48:48 +01:00
0719f3c4e3 Add small code style improvements
[MAILPOET-4761]
2023-01-30 09:36:21 +01:00
ba055b4278 Move static variable into a property
[MAILPOET-4761]
2023-01-30 09:36:21 +01:00
0b5a809883 Extend CouponPreProcessor unit test
[MAILPOET-4761]
2023-01-30 09:36:21 +01:00
a4f7a05bff Disable coupon generating when coupon id is set
[MAILPOET-4761]
2023-01-30 09:36:21 +01:00
1730578a23 Use coupon id instead of text
[MAILPOET-4761]
2023-01-30 09:36:21 +01:00
f88623e48d Add settings code and resetting placeholder
[MAILPOET-4761]
2023-01-30 09:36:21 +01:00
46fdd8eeb3 Add select with existing coupons
[MAILPOET-4761]
2023-01-30 09:36:21 +01:00
40f4216ff8 Add method for getting WC coupons
[MAILPOET-4761]
2023-01-30 09:36:21 +01:00
fe536fcdd0 Add coupon source switch
[MAILPOET-4761]
2023-01-30 09:36:21 +01:00
62cff7b388 Refactor analytics data campaigns_count to be more future proof
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
b24beb1dae Track only revenue data in the current shop currency
This is a protection against an edge case when a shop changes
the currency. We want to make sure we don't mix currencies.
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
b52c53f7f5 Add comment about revenue being tracked once per purchase
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
f66be1b947 Improve typehints and doctypes for the woo revenue tracking
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
5d3b26bb58 Remove unnecessary tracking enabled check
Registering the hook doesn't cost us much and it makes sense to register
it even when the tracking is disabled so that the data are always loaded
when calling: wp wc tracker snapshot --format=yaml
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
55b64d0354 Flatten array campaigns revenue tracking data
This format will be more suitable for further processing for Looker.
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
7ce1b6eb6c Cast numeric revenue tracking data
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
97b42a4a91 Add currency of the store to the tracking data
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
bedde323bd Add proper test group for the tracker integration test
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
0cf2787937 Add email campaigns count to WC tracker data
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
f0bc53766b Catch and log errors when fetching data in MailPoet Woo Tracker
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
f7fc2c16c1 Add Woo revenues to test/dev data generator for Woo revenues
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
f6d80b6e8b Fix type error in Woo orders data generator script
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
17d3f66316 Register filter for tracking revenue data
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
23e08ecb44 Add method for fetching revenue data for the WC Tracker
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
4dfdfc8423 Add dummy Woo Tracker class
[MAILPOET-5014]
2023-01-30 09:21:39 +01:00
5cdce529b5 Create integration docker containers prior running tests
This fixes Error response from daemon: Conflict. The container name "/wordpress_0".
which occasionally occurs when integration tests start.
We applied the same fix for the acceptance tests and it fixed the issue.
[MAILPOET-4947]
2023-01-30 09:15:23 +01:00
522b8a87db Fix flaky MailerLog testItTruncatesOutdatedEntriesWhenIncrementingSentCount
The test was setting the value that was later checked to the very edge of the allowed interval.
So if the tested method was executed a second later test failed.
This commit sets the tested value a second before the end or the interval so
there should be enough time for the method to run with the expected outcome.
[MAILPOET-4866]
2023-01-30 09:32:31 +02:00
6e8660dcc4 Update Readme with the remaining info
[MAILPOET-4944]
2023-01-26 16:46:58 +01:00
936b7fed74 Update Readme with test environment
[MAILPOET-4944]
2023-01-26 16:46:58 +01:00
d87794aa1f Reorder tests structure
[MAILPOET-4944]
2023-01-26 16:46:58 +01:00
8675aaef6e Update Readme with headless info 2023-01-26 16:46:58 +01:00
824e097639 Update Readme file 2023-01-26 16:46:58 +01:00
dce8e94f8e Add Readme file
[MAILPOET-4944]
2023-01-26 16:46:58 +01:00
e22ffcb5e3 Establishing k6 into mailpoet env
[MAILPOET-4944]
2023-01-26 16:46:58 +01:00
8f94096cdd Hide unnecessary close button, that causes vertical scrollbars in form editor on desktops
[MAILPOET-4607]
2023-01-26 14:45:33 +01:00
faa17b0d5c Add animation on close button in form editor
[MAILPOET-4607]
2023-01-26 14:45:33 +01:00
c4d4a6e594 Add bottom padding so the HelpScout icon never overlays editor's UI
[MAILPOET-4605]
2023-01-26 14:27:55 +01:00
9937bcc2d5 Remove custom HelpScout position in form editor
[MAILPOET-4605]
2023-01-26 14:27:55 +01:00
031c7d9866 Expose external link component
[PREMIUM-215]
2023-01-26 14:12:22 +01:00
0e84ddb957 Fix error styles for radio inputs
[PREMIUM-215]
2023-01-26 14:12:22 +01:00
5672823472 Export automation config for reuse
[PREMIUM-215]
2023-01-26 14:12:22 +01:00
d03e11f938 Use detected locale in localized strings
[PREMIUM-215]
2023-01-26 14:12:22 +01:00
3dbd91bfef Add Intl.Locale detection for formatting localized strings
[PREMIUM-215]
2023-01-26 14:12:22 +01:00
d0ff2a9eae Process also day format for custom fields
Although not currently offered in the UI, day format is partially implemented and passes validation.

[PREMIUM-215]
2023-01-26 14:12:22 +01:00
78314944aa Do not require default props in the premium plugin
This aligns the configuration with the free plugin.

[PREMIUM-215]
2023-01-26 14:12:22 +01:00
872bf07b25 Remove hover effect from plain titles
This is consisteng with Gutenberg editor behavior.

[PREMIUM-215]
2023-01-26 14:12:22 +01:00
17b79ee29f Expose textarea, radio, and select components
[PREMIUM-215]
2023-01-26 14:12:22 +01:00
f5b411e2ae Use plugin context for segments in automation UI
[PREMIUM-215]
2023-01-26 14:12:22 +01:00
e3e865eac5 Use plugin context to load list of segments
[PREMIUM-215]
2023-01-26 14:12:22 +01:00
6b7ffbc4ad Allow plugins to add their own context data for automation editor
[PREMIUM-215]
2023-01-26 14:12:22 +01:00
4ddcc14eee Rename context to registry
This will align better with the backend naming as well as free up "context" for other purposes.

[PREMIUM-215]
2023-01-26 14:12:22 +01:00
7bca78e268 Add placeholder step for subscriber update action
[PREMIUM-215]
2023-01-26 14:12:22 +01:00
82159dd4c5 Hide Screen options from Landing Page
[MAILPOET-5017]
2023-01-26 09:31:27 +01:00
1e76b214ea Don't load HelpScout Beacon on Dotcom Ecommerce plan
[MAILPOET-5017]
2023-01-26 09:31:27 +01:00
8c44364dab Hide Screen options from Welcome Wizard
[MAILPOET-5017]
2023-01-26 09:31:27 +01:00
7aea3528c4 Allow full-site-editing and wpcomsh plugins in ConflictResolver
[MAILPOET-5017]
2023-01-26 09:31:27 +01:00
b7809608d1 Fix distorted website icon on Dotcom
[MAILPOET-5017]
2023-01-26 09:31:27 +01:00
19bb957ae7 Fix confirmation link text
[MAILPOET-4931]
2023-01-25 20:22:08 +01:00
6e5ba84aab Disable visual editor for confirmation email in acceptance test
[MAILPOET-4931]
2023-01-25 20:22:08 +01:00
1a20f53916 Allow confirmation emails preview
[MAILPOET-4931]
2023-01-25 20:22:08 +01:00
57d364d142 Enable visual confirmation emails by default
[MAILPOET-4931]
2023-01-25 20:22:08 +01:00
89f76f67ca Rename old migrations
Because we expect that our plugin can recover from an invalid DB state.
We rename old migrations that ensure both migrations are executed again.
[MAILPOET-4962]
2023-01-25 20:21:13 +01:00
5e589cd5e3 Release 4.5.2 2023-01-25 13:52:19 -03:00
139fb2f1ad Hide Automations and Upgrade in landingpage
[MAILPOET-5019]
2023-01-25 13:42:43 +01:00
5e7ece1e87 Add mailpoet-landingpage to avoid redirection
[MAILPOET-5019]
2023-01-25 13:42:43 +01:00
47a4e3b4e9 Hide menu entries when not setup yet
[MAILPOET-5019]
2023-01-25 13:42:43 +01:00
0019ad110e Update DisplayFormInWPContent::display method type and add more tests
MAILPOET-4885
MAILPOET-4820
2023-01-25 13:15:08 +01:00
62325dc096 Fix multiple forms display on pages with Woocommerce [products] shortcode
MAILPOET-4885
2023-01-25 13:15:08 +01:00
290aceb0e9 Update copy of list visibility setting
[MAILPOET-4906]
2023-01-25 08:59:29 +01:00
a9f4e47fe0 Release 4.5.1 2023-01-24 11:47:42 -03:00
ede135c2b4 Update pnpm to the latest version
[MAILPOET-5016]
2023-01-24 15:51:21 +02:00
282 changed files with 6621 additions and 1609 deletions

View File

@ -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:

View File

@ -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]));
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -123,4 +123,5 @@
// This style hides the horizontal scrollbar in Firefox browser
.interface-interface-skeleton__sidebar {
overflow-x: hidden;
padding-bottom: 100px;
}

View 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;
}
}
}

View File

@ -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%;
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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';

View File

@ -21,3 +21,4 @@
@import 'components-homepage/_task-list';
@import 'components-homepage/_content-section';
@import 'components-homepage/_product-discovery';
@import 'components-homepage/_upsell';

View File

@ -1,3 +1,3 @@
// WordPress breakpoints
$mailpoet-breakpoint-small: 782px;
$mailpoet-breakpoint-small: 781px;
$mailpoet-breakpoint-medium: 960px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -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();

View File

@ -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) => (

View File

@ -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');
}
}
})();

View File

@ -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 && (

View File

@ -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 },

View File

@ -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[] {

View File

@ -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;

View File

@ -1,3 +1,4 @@
// exports for extensibility
export { id } from './id';
export * as config from './config';
export * as EditorStore from './editor/store';

View File

@ -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;

View File

@ -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();
};

View File

@ -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 ?? '';

View File

@ -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 =>

View File

@ -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;

View File

@ -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 subscribers 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;

View File

@ -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 (

View File

@ -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;

View File

@ -0,0 +1,2 @@
export * from './call_api';
export * from './track_event';

View File

@ -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';

View File

@ -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,
)}
</>
);
}

View 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,
})
}
/>
);
}

View File

@ -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';

View 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>
);
}

View File

@ -0,0 +1,2 @@
export * from './heading/heading';
export * from './list/list';

View File

@ -0,0 +1,3 @@
export const getLinkRegex = () => /\[link\](.*?)\[\/link\]/g;
export const isTruthy = (value: string | number | boolean) =>
[1, '1', true, 'true'].includes(value);

View File

@ -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,

View File

@ -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;
};

View File

@ -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 {

View File

@ -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';

View File

@ -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;

View File

@ -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';

View File

@ -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',
);

View File

@ -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);

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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';

View File

@ -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;

View File

@ -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>

View File

@ -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[];
}
>;
}

View File

@ -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';

View File

@ -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)}
</>
);
}

View 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>
)}
</>
);
}

View File

@ -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>
);
}

View 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>
);
}

View File

@ -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' };
}

View File

@ -21,3 +21,14 @@ export function saveProductDiscoveryDismissed() {
},
});
}
export function saveUpsellDismissed() {
return callApi({
endpoint: 'settings',
action: 'set',
method: 'POST',
data: {
'homepage.upsell_dismissed': true,
},
});
}

View File

@ -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,
};
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
};

View 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 };

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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 />

View File

@ -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();

View File

@ -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') : '',
});
}
});

View File

@ -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);
},
});

View File

@ -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) {

View File

@ -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;

View File

@ -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';

View File

@ -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);

View File

@ -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>
</>

View File

@ -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 };

View File

@ -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,

View File

@ -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=');

View File

@ -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

View File

@ -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')}

View File

@ -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 {

View File

@ -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,
}),
}),

View File

@ -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;

View File

@ -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';

View File

@ -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';

View 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;
}
}

View File

@ -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 };

View 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 };

View File

@ -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 };

View File

@ -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