fluentcrm / fluent-crm Goto Github PK
View Code? Open in Web Editor NEWFluentCRM Core Plugin
FluentCRM Core Plugin
Hey there,
I'm well aware open rates are flawed but I see something happening with FluentCRM that I haven't seen with any other email marketing service (and I've tried many such as: aweber, convertkit, mailchimp, kartra, activecampaign, mailerlite, etc...)
FluentCRM seems to be the only one that will automatically mark a random percentage of subscribers as having opened the email immediately after it is sent.
Here's a video:
https://www.loom.com/share/c6e08909aacf47ee89a85edcd7bcbf39
It would be very nice if there were a filter to allow customizing the funnel icons when selecting a new trigger. Right now we are stuck with the default trigger for custom categories which is not a good UI/UX experience.
Please consider changing AdminMenu.php
line 427 to
'funnel_cat_icons' => apply_filters('fluent_crm_funnel_icons',[
'wordpresstriggers' => fluentCrmMix('images/funnel_icons/wordpress.svg'),
'woocommerce' => fluentCrmMix('images/funnel_icons/woocommerce.svg'),
'lifterlms' => fluentCrmMix('images/funnel_icons/lifterlms.svg'),
'easydigitaldownloads' => fluentCrmMix('images/funnel_icons/easydigitaldownloads.svg'),
'learndash' => fluentCrmMix('images/funnel_icons/learndash.svg'),
'memberpress' => fluentCrmMix('images/funnel_icons/memberpress.svg'),
'paidmembershippro' => fluentCrmMix('images/funnel_icons/paidmembershippro.svg'),
'restrictcontentpro' => fluentCrmMix('images/funnel_icons/restrictcontentpro.svg'),
'tutorlms' => fluentCrmMix('images/funnel_icons/tutorlms.svg'),
'wishlistmember' => fluentCrmMix('images/funnel_icons/wishlistmember.svg'),
])
This will allow addition removal and customization of funnel icons without breaking anything for users who dont use a filter there.
Not much more to say here.
This applies for updating a contact with the PHP API as well as users who manage their preferences.
All campaign emails should be attributed to the new emails.
Currently, FluentCRM comes with the composer autoloader that searches files and directories at runtime.
This will cause significant performance hits on sites with many custom autoloader classes.
FluentCRM's composer autoloader must only handle it's only classes.
For that, you need to run:
composer dump-autoload --classmap-authoritative
during the release process.
If you need to update the autoloader because of new classes in a classmap package for example, you can use dump-autoload to do that without having to go through an install or update.
Additionally, it can dump an optimized autoloader that converts PSR-0/4 packages into classmap ones for performance reasons. In large applications with many classes, the autoloader can take up a substantial portion of every request's time. Using classmaps for everything is less convenient in development, but using this option you can still use PSR-0/4 for convenience and classmaps for performance.
https://getcomposer.org/doc/03-cli.md#dump-autoload-dumpautoload
The issue is the following: It has to be $user->user_email not $user->email
This is an instance of WP_USER. The email property does not exists and thus there will never be a subscriber for this funnel.
private function isProcessable($funnel, $user)
{
$conditions = $funnel->conditions;
// check update_type
$updateType = Arr::get($conditions, 'update_type');
// subscriber will always be null.
$subscriber = FunnelHelper::getSubscriber($user->email);
if ($updateType == 'skip_all_if_exist' && $subscriber) {
return false;
}
//...
}
Inside Handler.php
you have the sendEmails() method.
This has the following main for each loop.
foreach ($campaignEmails as $email) {
if ($this->reachedEmailLimitPerSecond()) {
$this->restartWhenOneSecondExceeds();
} else {
$response = Mailer::send($email->data());
$this->dispatchedWithinOneSecond++;
if (is_wp_error($response)) {
$failedIds[] = $email->id;
} else {
CampaignEmail::where('id', $email->id)->whereNot('status', 'failed')->update([
'status' => 'sent',
'updated_at' => current_time('mysql')
]);
$sentIds[] = $email->id;
}
}
}
Now lets look what Mailer::send()
returns.
public static function send($data)
{
$headers = static::buildHeaders($data);
if (apply_filters('fluentcrm_is_simulated_mail', false, $data, $headers)) {
return true;
}
return wp_mail(
$data['to']['email'],
$data['subject'],
$data['body'],
$headers
);
}
wp_mail()
by contract RETURNS A BOOLEAN
That means that:
$response = Mailer::send($email->data());
$this->dispatchedWithinOneSecond++;
if (is_wp_error($response)) {
$failedIds[] = $email->id;
}
WILL ALWAYS MARK THE EMAIL AS SUCCESSFULL
Its not possible that this function returns a WP_ERROR class. It returns booleans.
/**
* Sends an email, similar to PHP's mail function.
*
* A true return value does not automatically mean that the user received the
* email successfully. It just only means that the method used was able to
* process the request without any errors.
*
* The default content type is `text/plain` which does not allow using HTML.
* However, you can set the content type of the email by using the
* {@see 'wp_mail_content_type'} filter.
*
* The default charset is based on the charset used on the blog. The charset can
* be set using the {@see 'wp_mail_charset'} filter.
*
* @since 1.2.1
* @since 5.5.0 is_email() is used for email validation,
* instead of PHPMailer's default validator.
*
* @global PHPMailer\PHPMailer\PHPMailer $phpmailer
*
* @param string|string[] $to Array or comma-separated list of email addresses to send message.
* @param string $subject Email subject.
* @param string $message Message contents.
* @param string|string[] $headers Optional. Additional headers.
* @param string|string[] $attachments Optional. Paths to files to attach.
* @return bool Whether the email was sent successfully.
*/
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
// code
}
This is a javascript related issue. But since JS is minified I have no way to debug.
To replicate the issue do the following.
public function getFunnelConditionDefaults($funnel)
{
return [
//'update_type' => 'update', // skip_all_actions, skip_update_if_exist
//'user_roles' => [],
//'run_multiple' => 'yes'
];
}
public function getFunnelConditionDefaults($funnel)
{
return [
'foo' => 'bar'
//'update_type' => 'update', // skip_all_actions, skip_update_if_exist
//'user_roles' => [],
//'run_multiple' => 'yes'
];
}
The same issue probably happens with settings fields and the reason most probably is that inside BaseTrigger.php
the default values are always serialized with wp_parse_args
which does not give the desired result for an empty array.
public function prepareEditorDetails($funnel)
{
$funnel->settings = wp_parse_args($funnel->settings, $this->getFunnelSettingsDefaults());
$funnel->settingsFields = $this->getSettingsFields($funnel);
$funnel->conditions = wp_parse_args($funnel->conditions, $this->getFunnelConditionDefaults($funnel));
$funnel->conditionFields = $this->getConditionFields($funnel);
return $funnel;
}
ListsController::storeBulk
will throw an error because you are trying to do a foreach loop on a non array.
You accounted for this in the TagsController::storeBulk
method which works fine an allows creation in the frontend.
This slight change below will allow to create lists while creating a new ListApplied trigger.
$lists = $request->get('lists', []);
if(empty($lists)) {
$lists = $this->request->get('items', []);
}
foreach ($lists as $list) {
if (!$list['title'] || empty($list['title'])) {
continue;
}
if(empty($list['slug'])) {
$list['slug'] = $list['title'];
}
$list = Lists::updateOrCreate(
['slug' => sanitize_title($list['slug'], 'display')],
['title' => sanitize_text_field($list['title'])]
);
do_action('fluentcrm_list_created', $list->id);
}
Hello, we are missing some countries for one of our projects (mostly Kosovo), and I found a topic with more countries posted on the WP support page for the plugin (https://wordpress.org/support/topic/country-list-edited/)
Will you be adding more countries soon, or is there a smart way for us to add the missing countries?
When you visit a subscribers contact page and send him a custom email. The SubscriberController
totally disregards the subscriber status.
Also if sending the mail fails no feedback is given. The response is always 200 success. A CustomEmailCampaign is created every single time. You are missing return statements.
Inside SubscriberController::sendCustomEmail()
public function sendCustomEmail(Request $request, $contactId)
{
$contact = Subscriber::findOrFail($contactId);
if ($contact->status != 'subscribed') {
// YOU HAVE TO RETURN HERE.
$this->sendError([
'message' => __('Subscriber\'s status need to be subscribed.', 'fluent-crm')
]);
}
add_action('wp_mail_failed', function ($wpError) {
// THIS DOES NOTHING, WE ARE INSIDE THE CLOSURE SCOPE.
$this->sendError([
'message' => $wpError->get_error_message()
]);
}, 10, 1);
$newCampaign = $request->get('campaign');
unset($newCampaign['id']);
$campaign = CustomEmailCampaign::create($newCampaign);
$campaign->subscribe([$contactId], [
'status' => 'scheduled',
'scheduled_at' => current_time('mysql')
]);
do_action('fluentcrm_process_contact_jobs', $contact);
return [
'message' => __('Custom Email has been successfully sent', 'fluent-crm')
];
}
When you enable the option "Auto Sync User Data and Contact Data" and then update a contacts email address in fluent crm the new user record will loose the user_id column and thus will no longer be linked to the actual WordPress user account.
This does not happen without that option enabled.
The reason is the following code inside Subscriber.php
where you have an updating event registered on the model.
static::updating(function ($model) {
if ($model->user_id && Helper::isUserSyncEnabled()) {
// Email was updated so $user will be bool(false)
$user = get_user_by('email', $model->email);
if ($user) {
if ($model->first_name) {
update_user_meta($user->ID, 'first_name', $model->first_name);
}
if ($model->last_name) {
update_user_meta($user->ID, 'last_name', $model->last_name);
}
}
// $user->ID will be null. We lost the account connection.
$model->user_id = $user->ID; // in case user id mismatch
}
});
Since we already know that we have a user_id on the model we should do the following instead:
$user = get_user_by('email', $model->email);
if ( ! $user ) {
$user = get_user_by('id', $model->user_id);
}
Thus we will at least not loose the connection to the user account. Wheter we want to update the actual user account email is another question. This should probably be configurable as well.
The updated version could look like this:
if ($model->user_id && Helper::isUserSyncEnabled()) {
$user = get_user_by('email', $model->email);
$email_mismatch = false;
if ( ! $user ) {
$email_mismatch = true;
$user = get_user_by('id', $model->user_id);
}
if ($user) {
if ($model->first_name) {
update_user_meta($user->ID, 'first_name', $model->first_name);
}
if ($model->last_name) {
update_user_meta($user->ID, 'last_name', $model->last_name);
}
}
if ( $user && $email_mismatch && apply_filters('fluentcrm_update_wp_user_email_on_change', false ) ) {
$user->user_email = $model->email;
wp_update_user($user);
}
$model->user_id = $user->ID; // in case user id mismatch
}
function fluentcrm_update_subscriber_meta($subscriberId, $key, $value)
{
// more code
return SubscriberMeta::create([
'key' => $key,
'value' => $value,
'subscriber_id' => $subscriberId,
'created_at' => fluentCrmTimestamp()
]);
}
You are missing the created_by column here which is a non-nullable column without a default value. The only reason this does not throw a fatal mysqli error is because WordPress is so permissive with its sql mode.
But the user id is not recorded anyway and always set to 0 even if logged in.
It should be
return SubscriberMeta::create([
'key' => $key,
'value' => $value,
'created_by' => get_current_user_id(),
'subscriber_id' => $subscriberId,
'created_at' => fluentCrmTimestamp()
]);
There is an error in the API documentation in the wiki and on the FluentCRM Docs website.
To get contact by user_id, getContactByUserId( $user_id ) should be used, not getContact($user_id).
Best regards
Karl Emil Nikka
When sending email campaigns in fluent CRM...
During the "Subject & Settings" stage...
Entering a custom "Email Pre-Header" doesn't quite work properly.
For example, If I enter the text "Here are some words."
The email preview text in the inbox will display as follows:
"Here are some words.Then the text from my email is auto populated like this..."
I feel like if I enter a custom email pre-header, the preview text should only include what I've written and not grab any info from the email I've written.
Additionally, it would be awesome to be able to check a box that sends the email with blank preview text (as in no preview text is displayed in the inbox"
Currently to achieve the desired results (Custom preview text, or blank preview text) I need to include the following html code at the beginning of every email:
<div style="display: none; max-height: 0px; overflow: hidden;">
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
</div>
Thanks for your consideration!
-Kevin
how to add a section and field in settings?
i cant find a filter or hook.
Right now its only possible to register one callback per model event.
Ever successive call to Mode::registerModelEvent()
will overwritte all previous callbacks.
Current code:
public static function registerModelEvent($event, $callback)
{
$calledClass = static::getCalledClassName();
static::$events["model.{$calledClass}.{$event}"] = $callback;
}
What it should be:
public static function registerModelEvent($event, $callback)
{
$calledClass = static::getCalledClassName();
static::$events["model.{$calledClass}.{$event}"][] = $callback;
}
And then also something like this:
public function fireModelEvent($event)
{
$calledClass = static::getCalledClassName();
$eventName = "model.{$calledClass}.{$event}";
$return = null;
if (isset(static::$events[$eventName])) {
foreach (static::$events[$eventName] as $callback) {
$return = call_user_func($callback, $this);
if( $return === false ) {
return false;
}
}
return $return;
}
}
I assume you did not intend it to work this way to only be able to register one event per model action. Its kinda limiting and the fix is easy and non-breaking
Hi,I'm not sure it's the right place to report a issue here,I started to test fluentcrm a few days ago,first thing is setting up a submission form,and enable double opt-in email conformation,the vistiors submit their emails throught the form and captured in fluentcrm contacts,showing submisson status "pending" before the vistiors click the confirmation button in email,once they confirm the the submission,the stauts will change to "subscribed",everything is good so far.But after setting up an automation for sending a marketing email to the subscribers whos status are"subscribed",the form will ingore the pending status, subscribers don't need to click confirmation even they can get the double opt-in confirmation email.their submission status are"subscribed" already after they submit the email through that form.
After installing the plugin from wp.org repository to /wp-content/languages/plugins directory should download localization files corresponding to non-English site language. But with FluentCRM, this does not seem to happen.
public static function getPublicListIDs()
{
$emailSettings = Helper::getGlobalEmailSettings();
$lists = [];
$preListType = Arr::get($emailSettings, 'pref_list_type', 'none');
if ($preListType == 'filtered_only') {
$prefListItems = Arr::get($emailSettings, 'pref_list_items', []);
if ($prefListItems) {
$lists = Lists::whereIn('id', $prefListItems)->get();
if ($lists->isEmpty()) {
return [];
}
}
} else if ($preListType == 'all') {
$lists = Lists::get();
if ($lists->isEmpty()) {
return [];
}
}
return $lists;
}
This code is a private method inside ExternalPages.php
. I think everything that is related to the global settings should be kept inside the Helper class to keep things consistent.
Anyway, this is not something that should belong into ExternalPages.php
as its very useful to have access to in other parts of your application.
I just moved it inside Helper.php
I love this product!
Where is the best place to post grammar and spelling in the UI?
Edit: My timezone was off an hour which was causing this confusion:
I see this grammatical error popping up in several places. "23 minutes from now" references the future. "23 minutes ago" references the past.
The Gutenberg editor always fetches a new WordPress nonce when the current one is expired, but the user is still logged in.
FluentCRM should do the same.
The WP-Rest API will always return this error for expired nonces for logged-in users.
return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie check failed' ), array( 'status' => 403 ) );
From rest_cookie_check_errors
.
I think this is even handled automatically if you use the official client JS for the rest API.
// Send a refreshed nonce in header.
rest_get_server()->send_header( 'X-WP-Nonce', wp_create_nonce( 'wp_rest' ) );
Sorry if I'm posting this in the wrong place but could find it anywhere...
I have a few suggestions that probably can be implemented quickly and bring more flexibility to FluentCMS.
I'm a frequent user of other CRM systems and I've noticed almost immediatly a few important things:
Please don't get me wrong. I've been trying to use FluentCRM and learn about it before I'm putting any money on it... Some of this small things have a huge impact to the user experience. Just keep the good work and keep improving.
Hello, how are you guys? I have a problem here getting the unsubscription link working. In the preview it works but when I trigger the email the link is not applied and so the subscriber is unable to unsubscribe. How can I solve?
There is a type inside Stats::getCounts()
.
'tags' => [
'title' => __('Tags', 'fluent-crm'),
'count' => Tag::count(),
'route' => [
'name' => 'lags'
]
],
The route name should obv. be "tags".
Hello,
The action "Apply List" is not working in latest version 1.1.80. It was working in version 1.1.5. I have checked the plugin code and see code for add user to the particular list is no more available in the latest version.
You can see the previous plugin code for add user to the list here:
This is no more available in the latest version.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.