PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.3
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.3
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
← All changes | admin/class-wpfnl-admin.php +660 -41 3.10.9 → 3.13.3 View file →
@@ -42,8 +42,23 @@
42 42 */
43 43 const WPFNL_FEED_OPTION_KEY = 'wpfnl_remote_info_feed_data';
44 44
45 45 /**
46 + * Plugin file of the free Mail Mint plugin, as wordpress.org ships it.
47 + */
48 + const MAIL_MINT_BASENAME = 'mail-mint/mail-mint.php';
49 +
50 + /**
51 + * Plugin file of Mail Mint Pro.
52 + */
53 + const MAIL_MINT_PRO_BASENAME = 'mail-mint-pro/mail-mint-pro.php';
54 +
55 + /**
56 + * Plugin file of WPFunnels Pro.
57 + */
58 + const WPFNL_PRO_BASENAME = 'wpfunnels-pro/wpfnl-pro.php';
59 +
60 + /**
46 61 * The ID of this plugin.
47 62 *
48 63 * @since 1.0.0
49 64 * @access private
@@ -64,17 +79,20 @@
64 79 private $dependency_plugins;
65 80
66 81
67 82 /**
68 - * Page hooks
83 + * Screens still rendered by the legacy PHP/Vue admin, which is what the
84 + * legacy stylesheet and bundle are written against.
69 85 *
86 + * NOTE: 'toplevel_page_wpfunnels' is deliberately absent. That screen is the
87 + * React admin, which ships its own namespaced bundle and must not inherit
88 + * the legacy admin CSS. The funnel list, its trash tab and the Templates
89 + * page are routes of that same app now, so their hooks are gone from here
90 + * too.
91 + *
70 92 * @var array
71 93 */
72 94 private $page_hooks = [
73 - 'toplevel_page_wpf_templates',
74 - 'toplevel_page_wpfunnels',
75 - 'wpfunnels_page_wpf_templates',
76 - 'wpfunnels_page_trash_funnels',
77 95 'wpfunnels_page_wpf_feature_comparison',
78 96 'wpfunnels_page_wp_funnel_settings',
79 97 'wpfunnels_page_wpfnl_settings',
80 98 'wpfunnels_page_wp_global_store',
@@ -79,9 +97,8 @@
79 97 'wpfunnels_page_wpfnl_settings',
80 98 'wpfunnels_page_wp_global_store',
81 99 'wpfunnels_page_edit_funnel',
82 100 'wpfunnels_page_create_funnel',
83 - 'wpfunnels_page_wpfnl_settings',
84 101 'wpfunnels_page_wpfnl_automations',
85 102 'wpfunnels_page_wpfnl_addons',
86 103 'wpfunnels_page_store_checkout',
87 104 'wpfunnels_page_wpfunnels_integrations',
@@ -86,10 +103,8 @@
86 103 'wpfunnels_page_store_checkout',
87 104 'wpfunnels_page_wpfunnels_integrations',
88 105 'wpfunnels_page_wpf-license',
89 106 'wpfunnels_page_email-builder',
90 - 'wpfunnels_page_wp_funnels',
91 - 'wpfunnels_page_wpf_templates'
92 107 ];
93 108
94 109 /**
95 110 * Initialize the class and set its properties.
@@ -165,8 +180,9 @@
165 180 {
166 181 $batch_processor = new BatchProcessingController();
167 182 new MigrationManager($batch_processor);
168 183
184 +
169 185 // Run stat status sync migration directly (independent of Action Scheduler).
170 186 $stat_sync = new \WPFunnels\Admin\Migrations\StatStatusSyncMigration();
171 187 $stat_sync->run();
172 188 }
@@ -839,13 +855,8 @@
839 855 if ('wpfunnels_page_email-builder' === $hook) {
840 856 wp_enqueue_style($this->plugin_name . '-email-builder', plugin_dir_url(__FILE__) . 'assets/dist/email-builder/email-builder.css', [], $this->version, 'all');
841 857 }
842 858
843 - if ($this->is_current_page('wpfunnels')) {
844 - wp_enqueue_style('wpfnl-admin-app', plugin_dir_url(__FILE__) . 'assets/css/admin-app.css', [], $this->version, 'all');
845 - }
846 -
847 -
848 859 if (in_array($hook, $this->page_hooks)) {
849 860 wp_enqueue_style('wp-color-picker');
850 861 wp_enqueue_style($this->plugin_name . '-jquery-ui', plugin_dir_url(__FILE__) . 'assets/css/jquery-ui.min.css', [], $this->version, 'all');
851 862
@@ -855,8 +866,555 @@
855 866 }
856 867 }
857 868
858 869 /**
870 + * Screen hooks the React single-page admin renders on — the WPFunnels
871 + * dashboard, plus the alias slug it was first built under.
872 + *
873 + * The settings page is here too. It normally redirects to `#/settings`
874 + * before this ever runs, so nothing loads on the way through; the entry
875 + * matters for the case where that redirect is filtered off, when the page
876 + * renders the app itself and needs the app's assets to do it.
877 + *
878 + * @return array Hook suffixes.
879 + * @since 3.13.2
880 + */
881 + private function get_react_admin_hooks()
882 + {
883 + return array(
884 + 'toplevel_page_' . WPFNL_MAIN_PAGE_SLUG,
885 + 'wpfunnels_page_' . WPFNL_REACT_ADMIN_SLUG,
886 + 'wpfunnels_page_' . WPFNL_GLOBAL_SETTINGS_SLUG,
887 + );
888 + }
889 +
890 +
891 + /**
892 + * Resolve what the automation screen still needs before it can be used.
893 + *
894 + * Automations run on four things: WPFunnels and WPFunnels Pro with an
895 + * activated licence, and Mail Mint and Mail Mint Pro with an activated
896 + * licence. Each of the two Pro stacks walks the same ladder — not on the
897 + * site, on the site but switched off, running but unlicensed, satisfied —
898 + * and each rung needs a different next step, so the whole thing is resolved
899 + * here rather than in the React app: only PHP can tell an uninstalled plugin
900 + * from a deactivated one, and only PHP knows the licence state.
901 + *
902 + * @return array List of dependency descriptors, in the order they should be
903 + * presented.
904 + * @since 3.13.3
905 + */
906 + private function get_automation_dependencies()
907 + {
908 + return array(
909 + $this->get_wpfnl_pro_dependency(),
910 + $this->get_mail_mint_dependency(),
911 + );
912 + }
913 +
914 +
915 + /**
916 + * Dependency descriptor for the WPFunnels Pro half of the ladder.
917 + *
918 + * WPFunnels Pro is not on wordpress.org, so the "not installed" rung is a
919 + * purchase link rather than a one-click install.
920 + *
921 + * @return array Dependency descriptor.
922 + * @since 3.13.3
923 + */
924 + private function get_wpfnl_pro_dependency()
925 + {
926 + $base = array(
927 + 'id' => 'wpfunnels-pro',
928 + 'name' => __('WPFunnels Pro', 'wpfnl'),
929 + 'basename' => self::WPFNL_PRO_BASENAME,
930 + 'slug' => 'wpfunnels-pro',
931 + 'action' => '',
932 + 'url' => '',
933 + 'newTab' => false,
934 + );
935 +
936 + if ((bool) apply_filters('wpfunnels/is_pro_license_activated', false)) {
937 + return array_merge($base, array(
938 + 'state' => 'satisfied',
939 + 'message' => '',
940 + 'label' => '',
941 + ));
942 + }
943 +
944 + if (Wpfnl_functions::is_plugin_activated(self::WPFNL_PRO_BASENAME)) {
945 + return array_merge($base, array(
946 + 'state' => 'needs_license',
947 + 'message' => __('WPFunnels Pro is active, but its license has not been activated yet.', 'wpfnl'),
948 + 'label' => __('Activate License', 'wpfnl'),
949 + 'url' => esc_url_raw(add_query_arg(array('page' => 'wpf-license'), admin_url('admin.php'))),
950 + ));
951 + }
952 +
953 + if (Wpfnl_functions::is_plugin_installed(self::WPFNL_PRO_BASENAME)) {
954 + return array_merge($base, array(
955 + 'state' => 'needs_activation',
956 + 'message' => __('WPFunnels Pro is installed but currently inactive.', 'wpfnl'),
957 + 'label' => __('Activate WPFunnels Pro', 'wpfnl'),
958 + 'action' => 'activate',
959 + ));
960 + }
961 +
962 + return array_merge($base, array(
963 + 'state' => 'not_installed',
964 + 'message' => __('Automations need WPFunnels Pro, which is not installed on this site.', 'wpfnl'),
965 + 'label' => __('Upgrade to Pro', 'wpfnl'),
966 + 'url' => 'https://getwpfunnels.com/pricing/',
967 + 'newTab' => true,
968 + ));
969 + }
970 +
971 +
972 + /**
973 + * Dependency descriptor for the Mail Mint half of the ladder.
974 + *
975 + * The free plugin comes from wordpress.org, so its first two rungs are
976 + * one-click: wpfunnels/v1/activate-plugin installs the package when it is
977 + * missing and activates it either way. Mail Mint Pro is a paid plugin, so
978 + * its "not installed" rung is a purchase link.
979 + *
980 + * @return array Dependency descriptor.
981 + * @since 3.13.3
982 + */
983 + private function get_mail_mint_dependency()
984 + {
985 + $base = array(
986 + 'id' => 'mail-mint',
987 + 'name' => __('Mail Mint', 'wpfnl'),
988 + 'basename' => self::MAIL_MINT_BASENAME,
989 + 'slug' => 'mail-mint',
990 + 'action' => '',
991 + 'url' => '',
992 + 'newTab' => false,
993 + );
994 +
995 + if (Wpfnl_functions::is_mail_mint_pro_license_active() && Wpfnl_functions::is_mail_mint_free_active()) {
996 + return array_merge($base, array(
997 + 'state' => 'satisfied',
998 + 'message' => '',
999 + 'label' => '',
1000 + ));
1001 + }
1002 +
1003 + if (!Wpfnl_functions::is_plugin_installed(self::MAIL_MINT_BASENAME)) {
1004 + return array_merge($base, array(
1005 + 'state' => 'not_installed',
1006 + 'message' => __('Automations need Mail Mint, which is not installed on this site.', 'wpfnl'),
1007 + 'label' => __('Install Mail Mint', 'wpfnl'),
1008 + 'action' => 'install',
1009 + ));
1010 + }
1011 +
1012 + if (!Wpfnl_functions::is_mail_mint_free_active()) {
1013 + return array_merge($base, array(
1014 + 'state' => 'needs_activation',
1015 + 'message' => __('Mail Mint is installed but currently inactive.', 'wpfnl'),
1016 + 'label' => __('Activate Mail Mint', 'wpfnl'),
1017 + 'action' => 'activate',
1018 + ));
1019 + }
1020 +
1021 + // Mail Mint is running; the rest of the ladder is about its Pro plugin.
1022 + if (Wpfnl_functions::is_plugin_activated(self::MAIL_MINT_PRO_BASENAME)) {
1023 + return array_merge($base, array(
1024 + 'state' => 'needs_license',
1025 + 'name' => __('Mail Mint Pro', 'wpfnl'),
1026 + 'basename' => self::MAIL_MINT_PRO_BASENAME,
1027 + 'message' => __('Mail Mint Pro is active, but its license has not been activated yet.', 'wpfnl'),
1028 + 'label' => __('Activate License', 'wpfnl'),
1029 + 'url' => esc_url_raw(admin_url('admin.php?page=mrm-admin#/settings/license')),
1030 + ));
1031 + }
1032 +
1033 + if (Wpfnl_functions::is_plugin_installed(self::MAIL_MINT_PRO_BASENAME)) {
1034 + return array_merge($base, array(
1035 + 'state' => 'needs_activation',
1036 + 'name' => __('Mail Mint Pro', 'wpfnl'),
1037 + 'basename' => self::MAIL_MINT_PRO_BASENAME,
1038 + 'slug' => 'mail-mint-pro',
1039 + 'message' => __('Mail Mint Pro is installed but currently inactive.', 'wpfnl'),
1040 + 'label' => __('Activate Mail Mint Pro', 'wpfnl'),
1041 + 'action' => 'activate',
1042 + ));
1043 + }
1044 +
1045 + return array_merge($base, array(
1046 + 'state' => 'not_installed',
1047 + 'name' => __('Mail Mint Pro', 'wpfnl'),
1048 + 'basename' => self::MAIL_MINT_PRO_BASENAME,
1049 + 'slug' => 'mail-mint-pro',
1050 + 'message' => __('Automations need Mail Mint Pro with an activated license.', 'wpfnl'),
1051 + 'label' => __('Get Mail Mint Pro', 'wpfnl'),
1052 + 'url' => 'https://getwpfunnels.com/pricing/#mail-mint',
1053 + 'newTab' => true,
1054 + ));
1055 + }
1056 +
1057 +
1058 + /**
1059 + * Work out what the header's Pro call to action should say and where it
1060 + * should go.
1061 + *
1062 + * There are three ways an install can fall short of a licensed Pro, and each
1063 + * needs a different next step — telling someone to buy Pro when the plugin is
1064 + * sitting deactivated in their plugins list is a dead end. Resolved here
1065 + * rather than in the React app because the activation link needs a nonce and
1066 + * a capability check.
1067 + *
1068 + * @return array|null CTA descriptor, or null when Pro is fully licensed and
1069 + * no call to action belongs in the header.
1070 + * @since 3.13.2
1071 + */
1072 + private function get_pro_cta()
1073 + {
1074 + if (!function_exists('get_plugins')) {
1075 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
1076 + }
1077 +
1078 + $basename = 'wpfunnels-pro/wpfnl-pro.php';
1079 +
1080 + if ((bool) apply_filters('wpfunnels/is_pro_license_activated', false)) {
1081 + return null;
1082 + }
1083 +
1084 + // Installed and running, but unlicensed — the features stay locked until
1085 + // the licence is activated, so send them to the licence screen.
1086 + if (Wpfnl_functions::is_plugin_activated($basename)) {
1087 + return array(
1088 + 'state' => 'needs_license',
1089 + 'label' => __('Activate License', 'wpfnl'),
1090 + 'title' => __('Activate your WPFunnels Pro license to unlock Pro features', 'wpfnl'),
1091 + 'url' => esc_url_raw(add_query_arg(array('page' => 'wpf-license'), admin_url('admin.php'))),
1092 + 'newTab' => false,
1093 + );
1094 + }
1095 +
1096 + // Present but deactivated — one click away, no purchase needed.
1097 + if (Wpfnl_functions::is_plugin_installed($basename)) {
1098 + $plugins_url = admin_url('plugins.php');
1099 + $can_activate = current_user_can('activate_plugins');
1100 +
1101 + /*
1102 + * Built with add_query_arg rather than wp_nonce_url on purpose.
1103 + * wp_nonce_url() returns esc_html()'d output, so its separators come
1104 + * back as `&`. That is correct for a URL echoed straight into
1105 + * markup — the HTML parser decodes it — but this one travels through
1106 + * wp_localize_script and is applied by React with setAttribute,
1107 + * where nothing decodes entities. The literal `&` would split
1108 + * the query string into junk parameters, `plugin` and `_wpnonce`
1109 + * would arrive empty, and WordPress would answer the click with
1110 + * "The link you followed has expired."
1111 + *
1112 + * wp_localize_script's own html_entity_decode() does not save us
1113 + * either: it only walks top-level scalars, and this URL is nested
1114 + * inside the CTA array.
1115 + *
1116 + * Without the capability the one-click link would only bounce off a
1117 + * permission check, so point at the plugins list instead.
1118 + */
1119 + $url = $can_activate
1120 + ? add_query_arg(
1121 + array(
1122 + 'action' => 'activate',
1123 + 'plugin' => rawurlencode($basename),
1124 + '_wpnonce' => wp_create_nonce('activate-plugin_' . $basename),
1125 + ),
1126 + $plugins_url
1127 + )
1128 + : $plugins_url;
1129 +
1130 + return array(
1131 + 'state' => 'needs_activation',
1132 + 'label' => __('Activate Pro', 'wpfnl'),
1133 + 'title' => __('WPFunnels Pro is installed but not active — activate it to unlock Pro features', 'wpfnl'),
1134 + 'url' => esc_url_raw($url),
1135 + 'newTab' => false,
1136 + );
1137 + }
1138 +
1139 + // Not on the site at all.
1140 + return array(
1141 + 'state' => 'not_installed',
1142 + 'label' => __('Upgrade to Pro', 'wpfnl'),
1143 + 'title' => __('Upgrade to WPFunnels Pro', 'wpfnl'),
1144 + 'url' => 'https://getwpfunnels.com/pricing/',
1145 + 'newTab' => true,
1146 + );
1147 + }
1148 +
1149 +
1150 + /**
1151 + * Enqueue the React single-page admin bundle.
1152 + *
1153 + * Deliberately kept out of the `$page_hooks` group: the React admin ships
1154 + * its own namespaced stylesheet (every class prefixed `rwpfnl`) and must not
1155 + * inherit the legacy admin CSS, which is written against the Vue markup.
1156 + *
1157 + * @param string $hook Current admin screen hook suffix.
1158 + *
1159 + * @return void
1160 + * @since 3.13.2
1161 + */
1162 + /**
1163 + * Data the React template library screen needs.
1164 + *
1165 + * The same facts the Vue library read off `WPFunnelVars` and
1166 + * `template_library_object`, narrowed to what the standalone Templates page
1167 + * actually uses. The listing itself still comes from the REST API; this is
1168 + * only the context the listing is read in.
1169 + *
1170 + * @return array Template library context.
1171 + * @since 3.13.5
1172 + */
1173 + private function get_template_library_data()
1174 + {
1175 + $is_pro_license_active = (bool) apply_filters('wpfunnels/is_pro_license_activated', false);
1176 +
1177 + return array(
1178 + 'builder' => Wpfnl_functions::get_builder_type(),
1179 + 'isAnyPluginMissing' => Wpfnl_functions::is_any_plugin_missing(),
1180 + 'dependencyPlugins' => Wpfnl_functions::get_dependency_plugins_status(),
1181 + 'types' => Wpfnl_functions::get_template_types(),
1182 + // Store Checkout funnels have their own allowance, so they are not
1183 + // counted against this one.
1184 + 'activeFunnels' => Wpfnl_functions::count_non_store_checkout_funnels(),
1185 + // -1 once the licence lifts the cap.
1186 + 'allowedFunnels' => $is_pro_license_active ? -1 : 3,
1187 + // The remote-funnel add-on serves the library from elsewhere, and
1188 + // this screen then has nothing of its own to fetch.
1189 + 'isRemote' => Wpfnl_functions::maybe_remote_funnel(),
1190 + );
1191 + }
1192 +
1193 +
1194 + private function enqueue_react_admin_assets($hook)
1195 + {
1196 + if (!in_array($hook, $this->get_react_admin_hooks(), true)) {
1197 + return;
1198 + }
1199 +
1200 + /*
1201 + * A production build splits the webpack runtime into its own chunk that
1202 + * every entry depends on, so it has to load first. A development build
1203 + * (npm run start) keeps the runtime inside each entry and emits no such
1204 + * file, hence the existence check rather than a hard dependency.
1205 + */
1206 + $runtime_deps = array();
1207 + $runtime_path = WPFNL_DIR . '/admin/assets/dist/runtime/index.min.js';
1208 +
1209 + if (file_exists($runtime_path)) {
1210 + wp_enqueue_script(
1211 + 'wpfnl-webpack-runtime',
1212 + plugin_dir_url(__FILE__) . 'assets/dist/runtime/index.min.js',
1213 + array(),
1214 + $this->version,
1215 + true
1216 + );
1217 +
1218 + $runtime_deps[] = 'wpfnl-webpack-runtime';
1219 + }
1220 +
1221 + wp_enqueue_script(
1222 + 'wpfnl-react-admin',
1223 + plugin_dir_url(__FILE__) . 'assets/dist/js/wpfnl-react-admin.min.js',
1224 + $runtime_deps,
1225 + $this->version,
1226 + true
1227 + );
1228 +
1229 + Wpfnl_functions::enqueue_bundle_style('wpfnl-react-admin', 'wpfnl-react-admin-bundle');
1230 +
1231 + // The React admin renders the whole header action group itself — the
1232 + // AI Assistant drawer included (see src/admin/components/AiDrawer/) —
1233 + // so the Vue global-ai-drawer bundle is NOT loaded on this screen: it
1234 + // would find neither of its mount points (#wpfnl-header-actions,
1235 + // #wpfnl-global-ai-drawer), and loading it anyway only shipped a second
1236 + // copy of the group's markup and styles.
1237 + $_is_pro_license_active = (bool) apply_filters('wpfunnels/is_pro_license_activated', false);
1238 +
1239 + /*
1240 + * Everything after the page name in the title WordPress just printed —
1241 + * separator, site name, " — WordPress". The React admin is one WordPress
1242 + * page with its routes in the hash, so WordPress titles the tab once,
1243 + * with this page's own name ("Dashboard"), and every route below it
1244 + * inherits that. The app retitles itself per route (see
1245 + * src/admin/utils/documentTitle.js) and needs this tail, which is
1246 + * translatable and so cannot be rebuilt in JavaScript.
1247 + *
1248 + * `$admin_title` is built from `$title`, so it always starts with it;
1249 + * the prefix check is only there in case an `admin_title` filter has
1250 + * rewritten the whole thing, in which case the app keeps what WordPress
1251 + * printed for the page name it resolves. Entities such as `‹`
1252 + * survive here: wp_localize_script() decodes them on the way out.
1253 + */
1254 + $_document_title_suffix = '';
1255 +
1256 + if (isset($GLOBALS['title'], $GLOBALS['admin_title'])) {
1257 + $_page_title = (string) $GLOBALS['title'];
1258 + $_admin_title = (string) $GLOBALS['admin_title'];
1259 +
1260 + if ('' !== $_page_title && 0 === strpos($_admin_title, $_page_title)) {
1261 + $_document_title_suffix = substr($_admin_title, strlen($_page_title));
1262 + }
1263 + }
1264 +
1265 + // Mail Mint's install state is read from get_plugins() below.
1266 + if (!function_exists('get_plugins')) {
1267 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
1268 + }
1269 +
1270 + /*
1271 + * Core's plugin updater. The template library route offers to install
1272 + * the page builder its templates are authored for when that plugin is
1273 + * missing, and `wp.updates` is what does the installing.
1274 + */
1275 + wp_enqueue_script('updates');
1276 +
1277 + wp_localize_script(
1278 + 'wpfnl-react-admin',
1279 + 'RWPFNLAdmin',
1280 + array(
1281 + 'rest_api_url' => get_rest_url(),
1282 + 'nonce' => wp_create_nonce('wp_rest'),
1283 + 'security' => wp_create_nonce('wpfnl-admin'),
1284 + 'admin_url' => admin_url(),
1285 + 'image_path' => WPFNL_URL . 'admin/assets/images',
1286 + // Base URL the bundle resolves its async chunks against — the
1287 + // canvas editor is code-split (see src/admin/public-path.js).
1288 + 'dist_url' => WPFNL_URL . 'admin/assets/dist/',
1289 + 'priceConfig' => Wpfnl_functions::get_wc_price_config(),
1290 + 'isProActivated' => apply_filters('wpfunnels/is_wpfnl_pro_active', false),
1291 + 'isWPFIntegrationActive' => apply_filters('wpfunnels/is_integration_license_active', false),
1292 + /*
1293 + * Mail Mint's licence state, which decides whether the automation
1294 + * route shows the promo screen at all or hands the visitor to the
1295 + * legacy automation list.
1296 + */
1297 + 'isMailMintProLicenseActive' => Wpfnl_functions::is_mail_mint_pro_license_active(),
1298 + /*
1299 + * What the automation screen still needs, rung by rung, with the
1300 + * next step for each. Plugin-installing rungs carry a basename and
1301 + * slug the screen posts to wpfunnels/v1/activate-plugin; the rest
1302 + * carry a URL.
1303 + */
1304 + 'automationDependencies' => $this->get_automation_dependencies(),
1305 + 'canInstallPlugins' => current_user_can('install_plugins'),
1306 + 'canActivatePlugins' => current_user_can('activate_plugins'),
1307 + 'getText' => Wpfnl_functions::get_text(),
1308 + 'startOfWeek' => (int) get_option('start_of_week', 0),
1309 + /*
1310 + * Whether the onboarding checklist was on show last time the
1311 + * dashboard asked — 'yes', 'no', or absent when unknown. Only
1312 + * decides whether to hold space for it while its real status
1313 + * loads; the status itself still comes from the REST call.
1314 + */
1315 + 'onboardingHint' => get_transient(\WPFunnels\Rest\Controllers\DashboardController::ONBOARDING_VISIBILITY_KEY),
1316 + /*
1317 + * Header action group data. It has to ride on this handle, not on
1318 + * the AI-drawer handle the Vue group used: WordPress prints a
1319 + * script's localized data immediately before that script's own
1320 + * tag, and the React bundle is enqueued first — so anything
1321 + * attached to the drawer does not exist yet when React renders.
1322 + */
1323 + 'isProLicenseActive' => $_is_pro_license_active,
1324 + 'upgradeUrl' => 'https://getwpfunnels.com/pricing/',
1325 + 'proCta' => $this->get_pro_cta(),
1326 + 'documentTitleSuffix' => $_document_title_suffix,
1327 + /*
1328 + * What the template library route runs on. Every value here is
1329 + * something only PHP can answer — which page builder the site is
1330 + * set up for and whether its plugin is actually there, how much
1331 + * of an unlicensed install's funnel allowance is left, which
1332 + * template types this install has. Arrives as a real object:
1333 + * wp_localize_script only stringifies top-level scalars.
1334 + */
1335 + 'templateLibrary' => $this->get_template_library_data(),
1336 + /*
1337 + * The two settings panels that are still Vue apps of their own,
1338 + * fetched by the settings screen when one of them is opened.
1339 + */
1340 + 'settingsPanelApps' => $this->get_settings_panel_apps(),
1341 + )
1342 + );
1343 +
1344 + $this->enqueue_settings_panel_apps();
1345 + }
1346 +
1347 +
1348 + /**
1349 + * The two settings panels that are still Vue apps of their own.
1350 + *
1351 + * "AI Assistant" and "MCP for AI Agents" each own their fields and save them
1352 + * through their own REST routes, so the React settings screen renders a
1353 + * mount point and asks those bundles to mount into it rather than
1354 + * reimplementing them (see src/admin/pages/Settings/VuePanel.jsx). The
1355 + * bundles are unchanged — they still mount themselves on the legacy settings
1356 + * page, where the mount point is printed by PHP.
1357 + *
1358 + * @return void
1359 + * @since 3.13.8
1360 + */
1361 + private function enqueue_settings_panel_apps()
1362 + {
1363 + $_current_user = wp_get_current_user();
1364 + $_common = array(
1365 + 'username' => $_current_user ? $_current_user->user_login : '',
1366 + 'rest_url' => esc_url_raw(get_rest_url()),
1367 + 'nonce' => wp_create_nonce('wp_rest'),
1368 + 'admin_url' => esc_url_raw(admin_url()),
1369 + );
1370 +
1371 + /*
1372 + * The globals the two bundles read, printed before the React bundle so
1373 + * they are there whenever a bundle is pulled in later. They are a few
1374 + * hundred bytes; the bundles themselves are megabytes, which is why
1375 + * only the data is printed up front.
1376 + */
1377 + wp_add_inline_script(
1378 + 'wpfnl-react-admin',
1379 + 'window.WPFnlMcpSettings = ' . wp_json_encode(array_merge($_common, array('site_url' => esc_url_raw(home_url())))) . ';' .
1380 + 'window.WPFnlAiAssistantSettings = ' . wp_json_encode($_common) . ';',
1381 + 'before'
1382 + );
1383 + }
1384 +
1385 +
1386 + /**
1387 + * Where the settings screen fetches its two Vue panels from.
1388 + *
1389 + * Neither bundle is enqueued: only one settings panel in the app needs
1390 + * either of them, and the route is in the fragment, which the server never
1391 + * sees — so loading them on every screen of the admin would cost every
1392 + * screen megabytes for a panel most visitors never open. The panel loads
1393 + * what it needs when it is opened (see pages/Settings/panels/VuePanel.jsx).
1394 + *
1395 + * The stylesheet is empty on a development build, which injects its styles
1396 + * from JavaScript and emits none.
1397 + *
1398 + * @return array Script and stylesheet URL per panel.
1399 + * @since 3.13.8
1400 + */
1401 + private function get_settings_panel_apps()
1402 + {
1403 + return array(
1404 + 'aiAssistant' => array(
1405 + 'script' => plugin_dir_url(__FILE__) . 'assets/dist/js/ai-assistant-settings.min.js',
1406 + 'style' => Wpfnl_functions::get_bundle_style_url('ai-assistant-settings'),
1407 + ),
1408 + 'mcp' => array(
1409 + 'script' => plugin_dir_url(__FILE__) . 'assets/dist/js/mcp-agent-settings.min.js',
1410 + 'style' => Wpfnl_functions::get_bundle_style_url('mcp-agent-settings'),
1411 + ),
1412 + );
1413 + }
1414 +
1415 +
1416 + /**
859 1417 * Register the JavaScript for the admin area.
860 1418 *
861 1419 * @since 1.0.0
862 1420 */
@@ -877,8 +1435,11 @@
877 1435
878 1436 $screen = get_current_screen();
879 1437
880 1438
1439 + $this->enqueue_react_admin_assets($hook);
1440 +
1441 +
881 1442 if ('dashboard' === $screen->id || 'plugins' === $screen->id) {
882 1443 wp_enqueue_script($this->plugin_name . '-global', plugin_dir_url(__FILE__) . 'assets/js/wpfnl-global.js', ['jquery', 'jquery-ui-sortable'], $this->version, true);
883 1444 }
884 1445
@@ -910,10 +1471,65 @@
910 1471 )
911 1472 );
912 1473 }
913 1474
1475 + // Store Checkout without WooCommerce: power the "Plugin Required" modal
1476 + // rendered by admin/modules/store-checkout/views/woocommerce-required.php.
1477 + if ('wpfunnels_page_' . WPFNL_STORE_CHECKOUT_SLUG === $hook && !Wpfnl_functions::is_wc_active()) {
1478 + $wc_plugin = 'woocommerce/woocommerce.php';
1479 + $wc_status = Wpfnl_functions::is_plugin_installed($wc_plugin) ? 'inactive' : 'not-installed';
1480 + $wc_label = 'not-installed' === $wc_status
1481 + ? __('Install WooCommerce', 'wpfnl')
1482 + : __('Activate WooCommerce', 'wpfnl');
1483 +
1484 + wp_enqueue_script(
1485 + $this->plugin_name . '-store-checkout-woo',
1486 + plugin_dir_url(__FILE__) . 'assets/js/wpfnl-store-checkout-woo.js',
1487 + ['jquery', 'updates'],
1488 + $this->version,
1489 + true
1490 + );
1491 + wp_localize_script($this->plugin_name . '-store-checkout-woo', 'wpfnlStoreCheckoutWoo', array(
1492 + 'ajaxUrl' => admin_url('admin-ajax.php'),
1493 + 'nonce' => wp_create_nonce('wpfnl_activate_plugin_nonce'),
1494 + 'plugin' => $wc_plugin,
1495 + 'slug' => 'woocommerce',
1496 + 'status' => $wc_status,
1497 + 'buttonLabel' => $wc_label,
1498 + 'i18n' => array(
1499 + 'installing' => __('Installing', 'wpfnl'),
1500 + 'activating' => __('Activating', 'wpfnl'),
1501 + 'redirecting' => __('Redirecting', 'wpfnl'),
1502 + 'error' => __('Something went wrong. Please install and activate WooCommerce manually.', 'wpfnl'),
1503 + ),
1504 + ));
1505 + }
1506 +
1507 +
914 1508 if (in_array($hook, $this->page_hooks)) {
915 1509
1510 + // Admin header actions — the Knowledge Base, AI Assistant, Settings
1511 + // and Upgrade group rendered into #wpfnl-header-actions from every
1512 + // legacy WPFunnels admin screen. The data goes out under the
1513 + // WPFNAIFunnel global because that is the name
1514 + // admin/src/copilot/services/ai.js reads, and the drawer shares
1515 + // that client with what is left of the copilot bundle.
1516 + $_settings_page_url = admin_url( 'admin.php?page=' . WPFNL_MAIN_PAGE_SLUG ) . '#/settings';
1517 +
1518 + wp_enqueue_script('wpfnl-global-ai-drawer', plugin_dir_url(__FILE__) . 'assets/dist/js/global-ai-drawer.min.js', [], $this->version, true);
1519 + Wpfnl_functions::enqueue_bundle_style( 'global-ai-drawer', 'wpfnl-global-ai-drawer-bundle' );
1520 + wp_localize_script('wpfnl-global-ai-drawer', 'WPFNAIFunnel', array(
1521 + 'adminUrl' => esc_url_raw( admin_url( 'admin.php' ) ),
1522 + 'settingsPageUrl' => esc_url_raw( $_settings_page_url ),
1523 + 'isPro' => (bool) apply_filters( 'wpfunnels/is_pro_license_activated', false ),
1524 + 'upgradeUrl' => 'https://getwpfunnels.com/pricing/',
1525 + 'restApiUrl' => esc_url_raw( get_rest_url() ),
1526 + 'restNonce' => wp_create_nonce( 'wp_rest' ),
1527 + 'rest_url' => esc_url_raw( get_rest_url() ),
1528 + 'nonce' => wp_create_nonce( 'wp_rest' ),
1529 + 'rest_nonce' => wp_create_nonce( 'wp_rest' ),
1530 + ));
1531 +
916 1532 $is_wc_installed = 'no';
917 1533 $is_wpfunnels_installed = 'no';
918 1534 if (is_plugin_active('woocommerce/woocommerce.php')) {
919 1535 $is_wc_installed = 'yes';
@@ -971,15 +1587,19 @@
971 1587 wp_enqueue_media();
972 1588
973 1589 wp_enqueue_script($this->plugin_name . '-jed', plugin_dir_url(__FILE__) . 'assets/js/jed.js', ['jquery'], $this->version, true);
974 1590 wp_enqueue_script($this->plugin_name, plugin_dir_url(__FILE__) . 'assets/js/wpfnl-admin.js', ['jquery', 'jquery-ui-sortable'], $this->version, true);
1591 +
975 1592 wp_enqueue_script($this->plugin_name . '-runtime', plugin_dir_url(__FILE__) . 'assets/dist/runtime/index.min.js', ['jquery', 'wp-i18n', 'wp-util', 'updates', 'wp-color-picker', $this->plugin_name . '-jed'], $this->version, true);
976 1593 wp_enqueue_script($this->plugin_name . '-funnel-window', plugin_dir_url(__FILE__) . 'assets/dist/js/funnel-components.min.js', ['jquery', 'wp-i18n', 'wp-util', 'updates', 'wp-color-picker', $this->plugin_name . '-jed', $this->plugin_name . '-runtime'], $this->version, true);
1594 + Wpfnl_functions::enqueue_bundle_style( 'funnel-components', $this->plugin_name . '-funnel-window-bundle' );
977 1595 wp_enqueue_script($this->plugin_name . '-backbone-marionette', plugin_dir_url(__FILE__) . 'assets/lib/backbone/backbone.marionette.min.js', ['backbone'], $this->version, true);
978 1596 wp_enqueue_script($this->plugin_name . '-backbone-radio', plugin_dir_url(__FILE__) . 'assets/lib/backbone/backbone.radio.min.js', ['backbone'], $this->version, true);
979 1597
980 1598 $general_settings = Wpfnl_functions::get_general_settings();
981 1599 $builder = $general_settings['builder'];
1600 + $funnel_builder_mode = Wpfnl_functions::get_default_funnel_builder_mode();
1601 + $canvas_view_mode = Wpfnl_functions::get_canvas_view_mode();
982 1602
983 1603 /**
984 1604 * This code snippet will check if pro addons is activated or not. if not activated
985 1605 * Total number of funnels will be maximum 3, otherwise customer can add as more funnels
@@ -1042,8 +1662,10 @@
1042 1662 'totalAllowedStoreCheckouts' => $total_allowed_store_checkouts,
1043 1663 'product_url' => $product_url,
1044 1664 'totalAllowedFunnels' => $total_allowed_funnels,
1045 1665 'builder' => $builder,
1666 + 'funnel_builder_mode' => $funnel_builder_mode,
1667 + 'canvas_view_mode' => $canvas_view_mode,
1046 1668 'dependencyPlugins' => Wpfnl_functions::get_dependency_plugins_status(),
1047 1669 'isAnyPluginMissing' => Wpfnl_functions::is_any_plugin_missing(),
1048 1670 'isGlobalFunnelActivated' => Wpfnl_functions::is_global_funnel_activated(),
1049 1671 'isGlobalFunnel' => Wpfnl_functions::is_global_funnel($funnel_id),
@@ -1058,11 +1680,11 @@
1058 1680 'isAnyLmsPluginActive' => Wpfnl_functions::is_any_lms_plugin_active(),
1059 1681 'isLmsSettings' => Wpfnl_functions::is_enable_lms_settings(),
1060 1682 'isLmsDisbaled' => $this->maybe_lms_settings_disbaled(),
1061 1683 'lmsVersion' => (defined('WPFNL_PRO_LMS_VERSION') || defined('WPFUNNELS_PRO_LMS_VERSION')) && defined('LEARNDASH_VERSION') ? version_compare(LEARNDASH_VERSION, '4.2.1.1', '>=') : false,
1062 - 'global_funnel_type' => Wpfnl_functions::get_global_funnel_type(),
1063 1684 'individual_funnel_type' => get_post_meta($funnel_id, '_wpfnl_funnel_type', true),
1064 1685 'isStoreCheckout' => get_post_meta($funnel_id, '_wpfnl_funnel_type', true) === 'store_checkout',
1686 + 'isSaleFunnel' => get_post_meta($funnel_id, '_wpfnl_funnel_type', true) === 'wc' || get_post_meta($funnel_id, '_wpfnl_funnel_type', true) === 'store_checkout',
1065 1687 'gbf_set_condition_steps' => $this->get_gbf_steps($funnel_id),
1066 1688 'reconfigurable_condition_data' => $this->get_reconfigurable_condition_data($funnel_id),
1067 1689 'mint_steps' => $this->mint_step_settings($funnel_id),
1068 1690 'mint_tags' => Wpfnl_functions::get_mint_contact_groups('tags'),
@@ -1102,9 +1724,8 @@
1102 1724 : esc_url_raw(admin_url('admin.php?page=' . WPFNL_FUNNEL_PAGE_SLUG)),
1103 1725 'settings_url' => esc_url_raw(admin_url('admin.php?page=settings')),
1104 1726 'home_url' => esc_url_raw(home_url()),
1105 1727 'funnel_id' => $funnel_id,
1106 - 'isTemplatePage' => 'wpfunnels_page_wpf_templates' === $hook,
1107 1728 'is_pro' => Wpfnl_functions::is_pro_license_activated(),
1108 1729 'is_ab_tesing_available' => defined('WPFNL_PRO_VERSION') ? version_compare(WPFNL_PRO_VERSION, "1.7.3", ">=") : false,
1109 1730 'is_webhook_licensed' => Wpfnl_functions::is_webhook_license_activated(),
1110 1731 'pro_url' => add_query_arg('wpfnl-dashboard', '1', GETWPFUNNEL_PRICING_URL),
@@ -1157,25 +1778,9 @@
1157 1778 'shippingClasses' => Wpfnl_functions::get_shipping_classes(),
1158 1779 )
1159 1780 );
1160 1781
1161 - // Enqueue the admin dashboard script only on WPFunnels Main Page
1162 1782
1163 - if ($this->is_current_page('wpfunnels')) {
1164 - wp_enqueue_script('wpfnl-admin-app', plugin_dir_url(__FILE__) . 'assets/dist/js/wpfnl-admin-app.min.js', array(), $this->version, true);
1165 - wp_localize_script('wpfnl-admin-app', 'WPFAdminObj', array(
1166 - 'rest_api_url' => get_rest_url(),
1167 - 'security' => wp_create_nonce('wpfnl-admin'),
1168 - 'nonce' => wp_create_nonce('wp_rest'),
1169 - 'priceConfig' => Wpfnl_functions::get_wc_price_config(),
1170 - 'isProActivated' => apply_filters('wpfunnels/is_wpfnl_pro_active', false),
1171 - 'isWPFIntegrationActive' => apply_filters( 'wpfunnels/is_integration_license_active', false ),
1172 - 'getText' => Wpfnl_functions::get_text(),
1173 - 'admin_url' => admin_url(),
1174 - ));
1175 - }
1176 -
1177 -
1178 1783 /**
1179 1784 * Localize scripts for funnel window
1180 1785 *
1181 1786 * @var $localize
@@ -1226,9 +1831,11 @@
1226 1831 private function get_gbf_steps($funnel_id)
1227 1832 {
1228 1833 $gbf_steps = [];
1229 1834 if ($funnel_id) {
1230 - $is_gbf = get_post_meta($funnel_id, 'is_global_funnel', true);
1835 + $funnel_type = get_post_meta($funnel_id, '_wpfnl_funnel_type', true);
1836 + $is_gbf = get_post_meta($funnel_id, 'is_global_funnel', true);
1837 +
1231 1838 if ('yes' === $is_gbf) {
1232 1839 $steps = Wpfnl_functions::get_steps($funnel_id);
1233 1840 if (is_array($steps)) {
1234 1841 foreach ($steps as $step) {
@@ -1239,9 +1846,8 @@
1239 1846 array_push($gbf_steps, $step['id']);
1240 1847 }
1241 1848 }
1242 1849 elseif ('upsell' === $step['step_type'] || 'downsell' === $step['step_type']) {
1243 -
1244 1850 $conditions = get_post_meta($step['id'], 'global_funnel_' . $step['step_type'] . '_rules', true);
1245 1851 if (is_array($conditions)) {
1246 1852 array_push($gbf_steps, $step['id']);
1247 1853 }
@@ -1248,8 +1854,21 @@
1248 1854 }
1249 1855 }
1250 1856 }
1251 1857 }
1858 + } elseif ('store_checkout' === $funnel_type) {
1859 + $steps = Wpfnl_functions::get_steps($funnel_id);
1860 + if (is_array($steps)) {
1861 + foreach ($steps as $step) {
1862 + if (isset($step['id'], $step['step_type']) && 'checkout' === $step['step_type']) {
1863 + $raw = get_post_meta($step['id'], '_wpfnl_store_checkout_condition', true);
1864 + $condition = is_string($raw) ? json_decode($raw, true) : $raw;
1865 + if (is_array($condition) && isset($condition['condition_type']) && 'rules' === $condition['condition_type']) {
1866 + array_push($gbf_steps, $step['id']);
1867 + }
1868 + }
1869 + }
1870 + }
1252 1871 }
1253 1872 }
1254 1873
1255 1874 return $gbf_steps;
@@ -1603,17 +2222,11 @@
1603 2222 */
1604 2223 public function wpfnl_deactivated_plugin($filename)
1605 2224 {
1606 2225 if ('woocommerce/woocommerce.php' === $filename) {
1607 - if (!Wpfnl_functions::is_lms_addon_active()) {
1608 - Wpfnl_functions::update_funnel_type_to_lead();
1609 - }
1610 2226 Wpfnl_functions::wpfnl_delete_transient();
1611 2227 }
1612 2228 elseif ('sfwd-lms/sfwd_lms.php' === $filename) {
1613 - if (!Wpfnl_functions::is_wc_active()) {
1614 - Wpfnl_functions::update_funnel_type_to_lead();
1615 - }
1616 2229 Wpfnl_functions::wpfnl_delete_transient();
1617 2230 }
1618 2231 elseif ('wpfunnels-pro-lms/wpfunnels-pro-lms.php' === $filename) {
1619 2232 Wpfnl_functions::wpfnl_delete_transient();
@@ -1808,12 +2421,18 @@
1808 2421 {
1809 2422 // Check if we're on a WPFunnels admin page.
1810 2423 $screen = get_current_screen();
1811 2424
1812 - if ($screen && in_array($screen->id, $this->page_hooks)) {
2425 + if ($screen && (in_array($screen->id, $this->page_hooks) || 'wpfunnels_page_' . WPFNL_AI_FUNNEL_PAGE_SLUG === $screen->id)) {
1813 2426 $classes .= ' wpfunnels-body';
1814 2427 }
1815 -
2428 +
2429 + // The React admin styles the WordPress content column itself, so it gets
2430 + // its own class instead of the legacy one.
2431 + if ($screen && in_array($screen->id, $this->get_react_admin_hooks(), true)) {
2432 + $classes .= ' rwpfnl-page';
2433 + }
2434 +
1816 2435 return $classes;
1817 2436 }
1818 2437
1819 2438