PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 1.4.2
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v1.4.2
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
← All changes | admin/admin.php +198 -2707 2.12.51.4.2 View file →
@@ -6,26 +6,19 @@
6 6 */
7 7
8 8 namespace SRFM\Admin;
9 9
10 -use Astra_Notices;
10 +use SRFM\Admin\Views\Entries_List_Table;
11 +use SRFM\Admin\Views\Single_Entry;
11 12 use SRFM\Inc\AI_Form_Builder\AI_Helper;
12 13 use SRFM\Inc\Database\Tables\Entries;
13 -use SRFM\Inc\Global_Settings\Global_Settings;
14 14 use SRFM\Inc\Helper;
15 -use SRFM\Inc\Onboarding;
16 -use SRFM\Inc\Payments\Payment_Helper;
17 -use SRFM\Inc\Payments\Stripe\Stripe_Helper;
18 -use SRFM\Inc\Smart_Tags;
15 +use SRFM\Inc\Post_Types;
19 16 use SRFM\Inc\Traits\Get_Instance;
20 17
21 18 if ( ! defined( 'ABSPATH' ) ) {
22 19 exit; // Exit if accessed directly.
23 20 }
24 -
25 -if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
26 - require_once SRFM_DIR . 'inc/lib/astra-notices/class-bsf-admin-notices.php';
27 -}
28 21 /**
29 22 * Admin handler class.
30 23 *
31 24 * @since 0.0.1
@@ -33,99 +26,8 @@
33 26 class Admin {
34 27 use Get_Instance;
35 28
36 29 /**
37 - * Minimum number of forms or entries required to show the rating notice.
38 - *
39 - * @since 2.5.2
40 - */
41 - public const RATING_NOTICE_THRESHOLD = 3;
42 -
43 - /**
44 - * Post meta the Starter Templates (Astra Sites) plugin stamps on every post it
45 - * imports. The "Finish setting up" Thank You prompt (#3030) scopes to these
46 - * forms only. Owned by a plugin that is NOT a SureForms dependency: on installs
47 - * without Starter Templates nothing carries this meta and the prompt never shows.
48 - *
49 - * @since 2.12.4
50 - */
51 - public const ASTRA_SITES_IMPORT_META = '_astra_sites_imported_post';
52 -
53 - /**
54 - * Negative-cache transient: no form on this site carries the import marker.
55 - *
56 - * Set only when the marker query itself returns zero posts, which is a
57 - * site-wide fact rather than a per-user one, and cleared as soon as any post is
58 - * stamped with the marker (see invalidate_starter_template_cache()). This keeps
59 - * the query off the majority of installs without tying the features to whether
60 - * Starter Templates happens to still be active — the marker outlives it.
61 - *
62 - * @since 2.12.4
63 - */
64 - public const NO_IMPORTED_FORMS_TRANSIENT = 'srfm_no_starter_template_forms';
65 -
66 - /**
67 - * Inline CSS for Quill 1.x (react-quill) list markers.
68 - *
69 - * Quill 1.x renders bullet/numbered list markers via CSS ::before pseudo-elements,
70 - * whereas the vendor quill.snow.css targets .ql-ui child elements (Quill 2.x approach).
71 - * This constant is shared by enqueue_styles() and enqueue_scripts() to prevent drift.
72 - *
73 - * @since 2.5.2
74 - */
75 - public const QUILL_1X_INLINE_CSS = '.ql-editor ul,.ql-editor ol{padding-left:1.5em}.ql-editor ul>li,.ql-editor ol>li{list-style-type:none}.ql-editor ol li:not(.ql-direction-rtl),.ql-editor ul li:not(.ql-direction-rtl){padding-left:1.5em}.ql-editor ol li.ql-direction-rtl,.ql-editor ul li.ql-direction-rtl{padding-right:1.5em}.ql-editor ul>li::before{content:"\2022"}.ql-editor li::before{display:inline-block;white-space:nowrap;width:1.2em}.ql-editor li:not(.ql-direction-rtl)::before{margin-left:-1.5em;margin-right:.3em;text-align:right}.ql-editor li.ql-direction-rtl::before{margin-left:.3em;margin-right:-1.5em}.ql-editor ol li{counter-reset:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;counter-increment:list-0}.ql-editor ol li::before{content:counter(list-0,decimal) ". "}.ql-editor ol li.ql-indent-1{counter-increment:list-1;counter-reset:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-1::before{content:counter(list-1,lower-alpha) ". "}.ql-editor ol li.ql-indent-2{counter-increment:list-2;counter-reset:list-3 list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-2::before{content:counter(list-2,lower-roman) ". "}.ql-editor ol li.ql-indent-3{counter-increment:list-3;counter-reset:list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-3::before{content:counter(list-3,decimal) ". "}.ql-editor ol li.ql-indent-4{counter-increment:list-4;counter-reset:list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-4::before{content:counter(list-4,lower-alpha) ". "}.ql-editor ol li.ql-indent-5{counter-increment:list-5;counter-reset:list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-5::before{content:counter(list-5,lower-roman) ". "}.ql-editor ol li.ql-indent-6{counter-increment:list-6;counter-reset:list-7 list-8 list-9}.ql-editor ol li.ql-indent-6::before{content:counter(list-6,decimal) ". "}.ql-editor ol li.ql-indent-7{counter-increment:list-7;counter-reset:list-8 list-9}.ql-editor ol li.ql-indent-7::before{content:counter(list-7,lower-alpha) ". "}.ql-editor ol li.ql-indent-8{counter-increment:list-8;counter-reset:list-9}.ql-editor ol li.ql-indent-8::before{content:counter(list-8,lower-roman) ". "}.ql-editor ol li.ql-indent-9{counter-increment:list-9}.ql-editor ol li.ql-indent-9::before{content:counter(list-9,decimal) ". "}';
76 -
77 - /**
78 - * Dashboard widget entries data.
79 - *
80 - * @var array
81 - * @since 1.9.1
82 - */
83 - private $dashboard_widget_data = [];
84 -
85 - /**
86 - * Cached result for whether the rating notice should display.
87 - *
88 - * @var bool|null
89 - * @since 2.5.2
90 - */
91 - private $should_show_rating = null;
92 -
93 - /**
94 - * SureForms Page Default permission.
95 - *
96 - * @var string
97 - * @since 1.12.2
98 - */
99 - private static $sureforms_page_default_capability = 'manage_options';
100 -
101 - /**
102 - * Request memo for the "Finish setting up" Thank You prompt (#3030).
103 - *
104 - * A static property (not a function-local static) so tests can reset it via
105 - * reflection / reset_thankyou_prompt_cache() — otherwise the first call pins
106 - * the value for the whole process and the feature is untestable.
107 - *
108 - * @var array<int,array<string,mixed>>|null
109 - * @since 2.12.4
110 - */
111 - private static $thankyou_prompt_cache = null;
112 -
113 - /**
114 - * Request memo for the dashboard setup-checklist card (#3031).
115 - *
116 - * A static property (not a function-local static) so tests can reset it via
117 - * reset_form_setup_card_cache() and exercise the populated path — a
118 - * function-local static pins the first result for the whole process. Keyed by
119 - * user id since the payload derives from that user's capabilities.
120 - * `false` means "not computed yet"; `null`/array is a computed result.
121 - *
122 - * @var array<int,array<string,mixed>|null>
123 - * @since 2.12.4
124 - */
125 - private static $setup_card_cache = [];
126 -
127 - /**
128 30 * Class constructor.
129 31 *
130 32 * @return void
131 33 * @since 0.0.1
@@ -133,19 +35,9 @@
133 35 public function __construct() {
134 36 add_action( 'admin_menu', [ $this, 'add_menu_page' ], 9 );
135 37 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
136 38 add_action( 'admin_menu', [ $this, 'settings_page' ] );
137 - add_action( 'admin_menu', [ $this, 'add_learn_page' ] );
138 39 add_action( 'admin_menu', [ $this, 'add_new_form' ] );
139 - add_action( 'admin_menu', [ $this, 'add_suremail_page' ] );
140 - if ( ! Helper::has_pro() ) {
141 - add_action( 'admin_menu', [ $this, 'add_quiz_page' ] );
142 - add_action( 'admin_menu', [ $this, 'add_survey_reports_page' ] );
143 - add_action( 'admin_menu', [ $this, 'add_partial_entries_page' ] );
144 - add_action( 'admin_menu', [ $this, 'add_upgrade_to_pro' ] );
145 - add_action( 'admin_footer', [ $this, 'add_upgrade_to_pro_target_attr' ] );
146 - }
147 -
148 40 add_filter( 'plugin_action_links', [ $this, 'add_settings_link' ], 10, 2 );
149 41 add_action( 'enqueue_block_assets', [ $this, 'enqueue_styles' ] );
150 42 add_action( 'admin_head', [ $this, 'enqueue_header_styles' ] );
151 43 add_filter( 'admin_body_class', [ $this, 'admin_template_picker_body_class' ] );
@@ -153,864 +45,32 @@
153 45 // this action is used to restrict Spectra's quick action bar on SureForms CPTS.
154 46 add_action( 'uag_enable_quick_action_sidebar', [ $this, 'restrict_spectra_quick_action_bar' ] );
155 47
156 48 add_action( 'current_screen', [ $this, 'enable_gutenberg_for_sureforms' ], 100 );
157 - // Register notices early for React pages (before admin_enqueue_scripts).
158 - add_action( 'admin_init', [ $this, 'register_pro_compatibility_notices' ], 5 );
159 - // Display notices on traditional WordPress admin pages.
160 49 add_action( 'admin_notices', [ $this, 'srfm_pro_version_compatibility' ] );
161 50
51 + // Handle entry actions.
52 + add_action( 'admin_init', [ $this, 'handle_entry_actions' ] );
53 + add_action( 'admin_notices', [ Entries_List_Table::class, 'display_bulk_action_notice' ] );
54 +
55 + // This action enqueues translations for NPS Survey library.
56 + // A better solution will be required from library to resolve plugin conflict.
57 + add_action(
58 + 'admin_footer',
59 + static function() {
60 + Helper::register_script_translations( 'nps-survey-script' );
61 + },
62 + 1000
63 + );
162 64 // Enfold theme compatibility to enable block editor for SureForms post type.
163 65 add_filter( 'avf_use_block_editor_for_post', [ $this, 'enable_block_editor_in_enfold_theme' ] );
164 66
165 67 // Add action links to the plugin page.
166 68 add_filter( 'plugin_action_links_' . SRFM_BASENAME, [ $this, 'add_action_links' ] );
167 - // Check if admin notification is enabled and add entries badge.
168 - $general_options = get_option( 'srfm_general_settings_options', [] );
169 - $admin_notification_on = isset( $general_options['srfm_admin_notification'] ) ? (bool) $general_options['srfm_admin_notification'] : true;
170 -
171 - if ( $admin_notification_on ) {
172 - add_action( 'admin_menu', [ $this, 'maybe_add_entries_badge' ], 99 );
173 - }
174 -
175 69 add_filter( 'wpforms_current_user_can', [ $this, 'disable_wpforms_capabilities' ], 10, 3 );
176 -
177 - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_pointer' ] );
178 - // Ajax callbacks for wp-pointer functionality.
179 - add_action( 'wp_ajax_should_show_pointer', [ $this, 'pointer_should_show' ] );
180 - add_action( 'wp_ajax_sureforms_dismiss_pointer', [ $this, 'pointer_dismissed' ] );
181 - add_action( 'wp_ajax_sureforms_accept_cta', [ $this, 'pointer_accepted_cta' ] );
182 - add_action( 'wp_ajax_srfm_notice_response', [ $this, 'handle_notice_response' ] );
183 - add_action( 'wp_ajax_srfm_ai_widget_usage', [ $this, 'track_ai_widget_usage' ] );
184 -
185 - // Register dashboard widget only if there are recent entries.
186 - add_action( 'admin_init', [ $this, 'maybe_register_dashboard_widget' ] );
187 -
188 - // Enqueue the AI quick draft widget script on the dashboard screen.
189 - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_ai_dashboard_widget_assets' ] );
190 -
191 - // "Finish setting up" checklist widget on the main WP dashboard (#3031).
192 - add_action( 'wp_dashboard_setup', [ $this, 'register_form_setup_widget' ] );
193 - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_form_setup_widget_assets' ] );
194 -
195 - // Drop the "no imported forms" negative cache as soon as a post is stamped
196 - // with the import marker, so a template imported after the cache was written
197 - // surfaces immediately instead of waiting for the transient to expire.
198 - add_action( 'added_post_meta', [ $this, 'invalidate_starter_template_cache' ], 10, 3 );
199 - add_action( 'updated_post_meta', [ $this, 'invalidate_starter_template_cache' ], 10, 3 );
200 -
201 - // Save first form creation time stamp.
202 - add_action( 'admin_init', [ $this, 'save_first_form_creation_time_stamp' ] );
203 - add_action( 'admin_notices', [ $this, 'display_srfm_rating_notice' ] );
204 - add_action( 'admin_notices', [ $this, 'display_srfm_getting_started_notice' ] );
205 -
206 - // "Finish setting up" prompt, shown as an Astra Notices admin notice on
207 - // every admin screen except the dashboard (#3030).
208 - add_action( 'admin_notices', [ $this, 'render_thankyou_prompt_notice' ] );
209 -
210 - /**
211 - * Suppress foreign (third-party) admin notices on SureForms admin screens.
212 - *
213 - * Some plugins (e.g. Ninja Forms) print large promotional banners on every
214 - * admin page via the admin_notices / all_admin_notices / network_admin_notices
215 - * hooks. These bleed onto SureForms' own React admin screens and break the UI.
216 - * We run at the EARLIEST priority on each notice hook (all third-party
217 - * callbacks are registered before these hooks fire, during admin_init /
218 - * plugin load) and strip the foreign ones before they are echoed, while
219 - * preserving SureForms' own notices. Scoped strictly to SureForms screens.
220 - */
221 - add_action( 'admin_notices', [ $this, 'suppress_foreign_admin_notices' ], PHP_INT_MIN );
222 - add_action( 'all_admin_notices', [ $this, 'suppress_foreign_admin_notices' ], PHP_INT_MIN );
223 - add_action( 'network_admin_notices', [ $this, 'suppress_foreign_admin_notices' ], PHP_INT_MIN );
224 70 }
225 71
226 72 /**
227 - * Remove third-party admin notices on SureForms admin screens.
228 - *
229 - * Iterates over the callbacks registered on the admin notice hooks and
230 - * removes any that are not owned by SureForms. A callback is considered
231 - * owned by SureForms when it belongs to a class in the `SRFM` / `SRFM_PRO`
232 - * namespaces or to the bundled `BSF_Admin_Notices` / `Astra_Notices`
233 - * notices library. SureForms' own notices are therefore preserved while
234 - * foreign promotional banners are suppressed.
235 - *
236 - * This callback is hooked at `PHP_INT_MIN` so that it runs first on each
237 - * notice hook and removes the foreign callbacks before WordPress echoes
238 - * them (WP_Hook honours removals made during iteration). It is strictly
239 - * scoped to SureForms admin screens via {@see Helper::is_sureforms_admin_page()}
240 - * so no other admin page is affected.
241 - *
242 - * @since 2.10.0
243 - * @return void
244 - */
245 - public function suppress_foreign_admin_notices() {
246 - // Bail early if we are not on a SureForms admin screen. This keeps the
247 - // suppression strictly scoped and avoids touching any other admin page.
248 - // is_sureforms_admin_page() covers the core screens (dashboard, add-new,
249 - // settings, entries, the form CPT); we additionally match any admin page
250 - // whose `page` slug is SureForms-owned (sureforms_* / srfm_*) so the
251 - // suppression also applies to the payments/quiz/survey/learn/SMTP screens.
252 - if ( ! Helper::is_sureforms_admin_page() && ! $this->is_sureforms_owned_admin_page() ) {
253 - return;
254 - }
255 -
256 - global $wp_filter;
257 -
258 - // The hook currently being fired (admin_notices, all_admin_notices or network_admin_notices).
259 - $current_hook = current_action();
260 -
261 - if ( empty( $current_hook ) || empty( $wp_filter[ $current_hook ] ) || ! ( $wp_filter[ $current_hook ] instanceof \WP_Hook ) ) {
262 - return;
263 - }
264 -
265 - foreach ( $wp_filter[ $current_hook ]->callbacks as $priority => $callbacks ) {
266 - foreach ( $callbacks as $callback ) {
267 - $function = $callback['function'] ?? null;
268 -
269 - // Never remove our own suppression callback.
270 - if ( is_array( $function ) && isset( $function[0] ) && $function[0] === $this && 'suppress_foreign_admin_notices' === $function[1] ) {
271 - continue;
272 - }
273 -
274 - // Preserve SureForms-owned notices, remove everything else.
275 - if ( $this->is_sureforms_owned_notice_callback( $function ) ) {
276 - continue;
277 - }
278 -
279 - remove_action( $current_hook, $function, $priority );
280 - }
281 - }
282 - }
283 -
284 - /**
285 - * Get the first form creation time stamp.
286 - *
287 - * @since 1.10.1
288 - * @return int|false
289 - */
290 - public static function get_first_form_creation_time_stamp() {
291 - return Helper::get_srfm_option( 'first_form_created_at', false );
292 - }
293 -
294 - /**
295 - * Check if the first form has been created.
296 - *
297 - * @since 1.10.1
298 - * @return bool
299 - */
300 - public static function is_first_form_created() {
301 - // Convert the first form creation time stamp to a boolean. If it exists, it will return true, otherwise false.
302 - $first_form_creation_time_stamp = self::get_first_form_creation_time_stamp();
303 -
304 - // If the first form creation time stamp is not set, return false.
305 - if ( ! $first_form_creation_time_stamp ) {
306 - return false; // No forms created yet.
307 - }
308 -
309 - // Check if the first form creation time stamp is a valid integer and greater than zero.
310 - return is_int( $first_form_creation_time_stamp ) && $first_form_creation_time_stamp > 0;
311 - }
312 -
313 - /**
314 - * Whether a form's confirmation message is still the shipped default.
315 - *
316 - * Compared on tag-stripped, entity-decoded, whitespace-collapsed text rather
317 - * than raw HTML: the default is stored with a base64 icon on creation but
318 - * regenerated with a URL icon, so the markup differs while the wording does
319 - * not, and a starter-template import can store a literal apostrophe where the
320 - * generated default carries the encoded `&#039;` — decoding entities makes both
321 - * compare equal. Any real edit to the heading or body text changes the text and
322 - * flips this to false, which is exactly when the prompt should stop showing.
323 - *
324 - * Locale caveat: the comparison target is translated at call time, so a form
325 - * whose default was stored under a different active locale won't match. That
326 - * fails safe — the prompt simply doesn't show — never a false nag.
327 - *
328 - * @param int $form_id Form post ID.
329 - *
330 - * @since 2.12.4
331 - * @return bool
332 - */
333 - public static function is_default_confirmation_message( $form_id ) {
334 - $confirmation = get_post_meta( (int) $form_id, '_srfm_form_confirmation', true );
335 -
336 - if ( ! is_array( $confirmation ) || ! isset( $confirmation[0]['message'] ) || ! is_string( $confirmation[0]['message'] ) ) {
337 - return false;
338 - }
339 -
340 - // The default message is only ever shown for a "same page" confirmation.
341 - // A redirect ("different page" / "custom url") never renders it, yet the
342 - // stored settings still seed the default message string — so without this
343 - // guard a redirect form would be nagged forever about a message no visitor
344 - // sees, with no way to clear the prompt by doing what it asks.
345 - if ( ! isset( $confirmation[0]['confirmation_type'] ) || 'same page' !== $confirmation[0]['confirmation_type'] ) {
346 - return false;
347 - }
348 -
349 - $message = $confirmation[0]['message'];
350 -
351 - if ( '' === trim( $message ) ) {
352 - return false;
353 - }
354 -
355 - $normalize = static function ( $html ) {
356 - // Decode entities too, so an encoded apostrophe (&#039;) in the generated
357 - // default matches a literal one stored by a template import.
358 - $text = html_entity_decode( wp_strip_all_tags( (string) $html ), ENT_QUOTES, 'UTF-8' );
359 - return trim( (string) preg_replace( '/\s+/', ' ', $text ) );
360 - };
361 -
362 - return $normalize( $message ) === $normalize( Global_Settings::get_default_confirmation_message() );
363 - }
364 -
365 - /**
366 - * Whether a form has somewhere to send replies (an enabled email notification
367 - * with a non-empty recipient).
368 - *
369 - * @param int $form_id Form post ID.
370 - *
371 - * @since 2.12.4
372 - * @return bool
373 - */
374 - public static function form_has_reply_destination( $form_id ) {
375 - $notifications = get_post_meta( (int) $form_id, '_srfm_email_notification', true );
376 -
377 - if ( ! is_array( $notifications ) ) {
378 - return false;
379 - }
380 -
381 - foreach ( $notifications as $notification ) {
382 - if ( is_array( $notification ) && ! empty( $notification['status'] ) && ! empty( $notification['email_to'] ) ) {
383 - return true;
384 - }
385 - }
386 -
387 - return false;
388 - }
389 -
390 - /**
391 - * The most recently created form still needing setup (default Thank You
392 - * message, or no reply destination).
393 - *
394 - * Powers the "Finish setting up" prompt (#3030). Limited to the single latest
395 - * such form to avoid clutter, and to forms the current user may actually edit.
396 - * A form is a candidate when it still has an unfinished step (default Thank You
397 - * message, or no reply destination). Dismissal is enforced by the caller,
398 - * before this query runs.
399 - *
400 - * @since 2.12.4
401 - * @return array<int,array<string,mixed>> One entry, or none.
402 - */
403 - public static function get_thankyou_prompt_forms() {
404 - // Memoized for the request so repeated reads (e.g. the notice render plus
405 - // any add-on consumer) share a single query. Sentinel is null, not false,
406 - // so a filter returning false (__return_false to disable) still memoizes.
407 - if ( null !== self::$thankyou_prompt_cache ) {
408 - return self::$thankyou_prompt_cache;
409 - }
410 -
411 - /**
412 - * Filter the forms the "Finish setting up" Thank You notice may surface.
413 - *
414 - * @param array<int,array<string,mixed>> $prompts Candidate prompt payloads.
415 - *
416 - * @since 2.12.4
417 - */
418 - $filtered = apply_filters( 'srfm_thankyou_prompt_forms', self::compute_thankyou_prompt_forms() );
419 - self::$thankyou_prompt_cache = is_array( $filtered ) ? $filtered : [];
420 -
421 - return self::$thankyou_prompt_cache;
422 - }
423 -
424 - /**
425 - * Clear the request memo for the Thank You prompt (#3030).
426 - *
427 - * Lets tests exercise the memoized public path, and is a safe hook for anything
428 - * that changes which form qualifies (e.g. a form save).
429 - *
430 - * @since 2.12.4
431 - * @return void
432 - */
433 - public static function reset_thankyou_prompt_cache() {
434 - self::$thankyou_prompt_cache = null;
435 - }
436 -
437 - /**
438 - * Setup-checklist data for the newest starter-template form (#3031).
439 - *
440 - * Picks the most recent form the current user can edit that was created from an
441 - * Astra Sites starter template. The widget
442 - * lists a fixed set of optional next-steps for it — their completion is not
443 - * computed — so the payload carries only the form and the CTA targets. Memoized
444 - * for the request so the widget register/enqueue/render passes share one query.
445 - *
446 - * @since 2.12.4
447 - * @return array<string,mixed>|null Card payload, or null when there is no candidate form.
448 - */
449 - public static function get_form_setup_card() {
450 - $user_id = get_current_user_id();
451 -
452 - // Request memo, keyed per user — the payload derives from that user's
453 - // capabilities. Reset via reset_form_setup_card_cache().
454 - if ( array_key_exists( $user_id, self::$setup_card_cache ) ) {
455 - return self::$setup_card_cache[ $user_id ];
456 - }
457 -
458 - self::$setup_card_cache[ $user_id ] = self::compute_form_setup_card();
459 -
460 - return self::$setup_card_cache[ $user_id ];
461 - }
462 -
463 - /**
464 - * Drop the "no imported forms" negative cache when the marker is written.
465 - *
466 - * Hooked to added_post_meta/updated_post_meta. Without this, a starter template
467 - * imported after the negative cache was written would show neither the Thank You
468 - * prompt nor the setup widget until the transient expired.
469 - *
470 - * Arguments are read from func_get_args() rather than declared: the hook passes
471 - * ( $meta_id, $post_id, $meta_key ) and the meta id is never needed, so declaring
472 - * it would leave an unused parameter that the coding-standards gate rejects.
473 - *
474 - * @since 2.12.4
475 - * @return void
476 - */
477 - public function invalidate_starter_template_cache() {
478 - $args = func_get_args();
479 - $post_id = isset( $args[1] ) ? (int) $args[1] : 0;
480 - $meta_key = isset( $args[2] ) ? (string) $args[2] : '';
481 -
482 - if ( self::ASTRA_SITES_IMPORT_META !== $meta_key ) {
483 - return;
484 - }
485 -
486 - // A full-site import stamps this marker on every post it creates, so narrow to
487 - // our own post type: both features only ever query sureforms_form, and this
488 - // avoids clearing the cache repeatedly for pages and products during an import.
489 - if ( ! defined( 'SRFM_FORMS_POST_TYPE' ) || SRFM_FORMS_POST_TYPE !== get_post_type( $post_id ) ) {
490 - return;
491 - }
492 -
493 - delete_transient( self::NO_IMPORTED_FORMS_TRANSIENT );
494 - }
495 -
496 - /**
497 - * Clear the setup-card request memo (#3031).
498 - *
499 - * Lets tests exercise the populated path, and is a safe hook for anything that
500 - * changes which form qualifies (e.g. a form save).
501 - *
502 - * @since 2.12.4
503 - * @return void
504 - */
505 - public static function reset_form_setup_card_cache() {
506 - self::$setup_card_cache = [];
507 - }
508 -
509 - /**
510 - * REST handler: record a "Finish setting up" widget interaction (#3031).
511 - *
512 - * Records the clicked CTA/view action as an analytics event. The request
513 - * carries the displayed form id: capability is re-checked against it here —
514 - * beyond the route's generic permission callback — so only a genuine editor of
515 - * that form can act.
516 - *
517 - * @param \WP_REST_Request<array<string,mixed>> $request Request.
518 - *
519 - * @since 2.12.4
520 - * @return \WP_REST_Response|\WP_Error
521 - */
522 - public function dismiss_form_setup_card( $request ) {
523 - $form_id = absint( $request->get_param( 'form_id' ) );
524 -
525 - if ( $form_id <= 0 || ! defined( 'SRFM_FORMS_POST_TYPE' ) || SRFM_FORMS_POST_TYPE !== get_post_type( $form_id ) || ! current_user_can( 'edit_post', $form_id ) ) {
526 - return new \WP_Error( 'srfm_setup_card_forbidden', __( 'You are not allowed to update this prompt.', 'sureforms' ), [ 'status' => 403 ] );
527 - }
528 -
529 - $action = sanitize_key( (string) $request->get_param( 'action' ) );
530 -
531 - // Interaction analytics for the setup widget (#3031). Each event carries a
532 - // date automatically (see BSF_Analytics_Events::track()) and dedupes per
533 - // event name, matching the sibling notice's telemetry.
534 - $events = [
535 - 'edit_form' => 'form_setup_widget_edit_form',
536 - 'edit_thankyou' => 'form_setup_widget_edit_thankyou',
537 - 'set_up_email' => 'form_setup_widget_set_up_email',
538 - 'view_form' => 'form_setup_widget_view_form',
539 - ];
540 -
541 - if ( isset( $events[ $action ] ) ) {
542 - Analytics::events()->track( $events[ $action ], (string) $form_id );
543 - }
544 -
545 - return new \WP_REST_Response( [ 'success' => true ], 200 );
546 - }
547 -
548 - /**
549 - * Register the "Finish setting up" checklist widget on the main WP dashboard (#3031).
550 - *
551 - * Only for capable users, and only when there is a form still needing setup —
552 - * so the widget never appears empty. The data is memoized in get_form_setup_card()
553 - * and reused by the enqueue and render passes.
554 - *
555 - * @since 2.12.4
556 - * @return void
557 - */
558 - public function register_form_setup_widget() {
559 - if ( ! Helper::current_user_can() ) {
560 - return;
561 - }
562 -
563 - if ( null === self::get_form_setup_card() ) {
564 - return;
565 - }
566 -
567 - wp_add_dashboard_widget(
568 - 'srfm_form_setup_checklist',
569 - __( 'Finish setting up your form', 'sureforms' ),
570 - [ $this, 'render_form_setup_widget' ],
571 - null,
572 - null,
573 - 'normal',
574 - 'high'
575 - );
576 - }
577 -
578 - /**
579 - * Render the setup-checklist widget content (#3031).
580 - *
581 - * A heading (with a link to view the form), a subtitle, and a fixed list of
582 - * optional next-steps — each always shown with its CTA; completion is not
583 - * computed. Each CTA deep-links into the editor (edit form / Thank You message /
584 - * email notification) and records an analytics event via the REST endpoint
585 - * wired in the enqueued inline script.
586 - *
587 - * @since 2.12.4
588 - * @return void
589 - */
590 - public function render_form_setup_widget() {
591 - $card = self::get_form_setup_card();
592 -
593 - if ( null === $card ) {
594 - return;
595 - }
596 -
597 - // Optional next-steps — always offered, their completion is not computed.
598 - // 'event' is the analytics action key beaconed on click (see the widget JS).
599 - $rows = [
600 - [
601 - 'label' => __( 'Review or edit your form', 'sureforms' ),
602 - 'cta' => __( 'Edit form', 'sureforms' ),
603 - 'url' => $card['edit_url'],
604 - 'event' => 'edit_form',
605 - ],
606 - [
607 - 'label' => __( 'Personalize the Thank You message', 'sureforms' ),
608 - 'cta' => __( 'Edit message', 'sureforms' ),
609 - 'url' => $card['thankyou_url'],
610 - 'event' => 'edit_thankyou',
611 - ],
612 - [
613 - 'label' => __( 'Choose who gets notified of new replies', 'sureforms' ),
614 - 'cta' => __( 'Set up email', 'sureforms' ),
615 - 'url' => $card['email_url'],
616 - 'event' => 'set_up_email',
617 - ],
618 - ];
619 -
620 - // Fall back to a generic label for an untitled form so the heading never
621 - // renders "Finish setting up " with a dangling space.
622 - $card_title = '' !== trim( (string) $card['title'] ) ? $card['title'] : __( 'your form', 'sureforms' );
623 - $heading = sprintf(
624 - /* translators: %s: form title. */
625 - __( 'Finish setting up %s', 'sureforms' ),
626 - $card_title
627 - );
628 - ?>
629 - <div class="srfm-setup-checklist" id="srfm-setup-checklist">
630 - <p class="srfm-setup-checklist__title">
631 - <?php echo esc_html( $heading ); ?>
632 - <?php if ( ! empty( $card['view_url'] ) ) { ?>
633 - <a class="srfm-setup-checklist__view" data-srfm-event="view_form" href="<?php echo esc_url( $card['view_url'] ); ?>" target="_blank" rel="noopener noreferrer" aria-label="<?php echo esc_attr( sprintf( /* translators: %s: form title. */ __( 'View %s (opens in a new tab)', 'sureforms' ), $card_title ) ); ?>">
634 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
635 - </a>
636 - <?php } ?>
637 - </p>
638 - <p class="srfm-setup-checklist__subtitle"><?php esc_html_e( 'Customize your form to get it ready for real submissions:', 'sureforms' ); ?></p>
639 -
640 - <ul class="srfm-setup-checklist__steps">
641 - <?php foreach ( $rows as $row ) { ?>
642 - <li class="srfm-setup-checklist__step">
643 - <span class="srfm-setup-checklist__label"><?php echo esc_html( $row['label'] ); ?></span>
644 - <a class="srfm-setup-checklist__cta" data-srfm-event="<?php echo esc_attr( $row['event'] ); ?>" href="<?php echo esc_url( $row['url'] ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $row['cta'] ); ?></a>
645 - </li>
646 - <?php } ?>
647 - </ul>
648 - </div>
649 - <?php
650 - }
651 -
652 - /**
653 - * Enqueue the setup-checklist widget's styles and behavior on the dashboard (#3031).
654 - *
655 - * Mirrors the AI widget convention: an inline-only handle carries the CSS and the
656 - * behavior (CTA click analytics), with server values —
657 - * the REST URL, nonce and form id — passed through wp_localize_script rather than
658 - * printed into the markup, so it stays Plugin-Check clean.
659 - *
660 - * @param string $hook_suffix Current admin page hook suffix.
661 - *
662 - * @since 2.12.4
663 - * @return void
664 - */
665 - public function enqueue_form_setup_widget_assets( $hook_suffix ) {
666 - if ( 'index.php' !== $hook_suffix || ! Helper::current_user_can() ) {
667 - return;
668 - }
669 -
670 - $card = self::get_form_setup_card();
671 -
672 - if ( null === $card ) {
673 - return;
674 - }
675 -
676 - $css = <<<'CSS'
677 -#srfm_form_setup_checklist .inside { margin: 0; padding: 0; }
678 -.srfm-setup-checklist { padding: 12px 16px 16px; }
679 -.srfm-setup-checklist__title { margin: 0 0 4px; font-size: 15px; font-weight: 600; color: #1e1e1e; }
680 -.srfm-setup-checklist__view { display: inline-flex; align-items: center; margin-left: 6px; color: #d54e21; vertical-align: middle; }
681 -.srfm-setup-checklist__view:hover, .srfm-setup-checklist__view:focus { color: #b83c14; }
682 -.srfm-setup-checklist__subtitle { margin: 0 0 12px; color: #646970; font-size: 13px; }
683 -.srfm-setup-checklist__steps { margin: 0; padding: 0; list-style: none; }
684 -.srfm-setup-checklist__step { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 8px; }
685 -.srfm-setup-checklist__step + .srfm-setup-checklist__step { margin-top: 6px; }
686 -.srfm-setup-checklist__step { background: #f6f7f7; }
687 -.srfm-setup-checklist__label { flex: 1 1 auto; font-size: 14px; color: #1e1e1e; }
688 -.srfm-setup-checklist__cta { margin-left: auto; border: 0; background: transparent; padding: 0; font-size: 14px; font-weight: 600; color: #d54e21; text-decoration: underline; cursor: pointer; }
689 -.srfm-setup-checklist__cta:hover { color: #b83c14; }
690 -/* Keep visited links on-brand — WP admin's a:visited would otherwise turn them blue. */
691 -.srfm-setup-checklist a:visited { color: #d54e21; }
692 -.srfm-setup-checklist a:visited:hover, .srfm-setup-checklist a:visited:focus { color: #b83c14; }
693 -/* Drop WP's blue focus ring on the widget's links; keep an accessible, on-brand keyboard outline. */
694 -.srfm-setup-checklist a:focus { outline: none; box-shadow: none; }
695 -.srfm-setup-checklist a:focus-visible { outline: 2px solid #d54e21; outline-offset: 2px; box-shadow: none; }
696 -CSS;
697 -
698 - wp_register_style( 'srfm-setup-checklist-widget', false, [], SRFM_VER );
699 - wp_enqueue_style( 'srfm-setup-checklist-widget' );
700 - wp_add_inline_style( 'srfm-setup-checklist-widget', $css );
701 -
702 - wp_register_script( 'srfm-setup-checklist-widget', '', [], SRFM_VER, true );
703 - wp_enqueue_script( 'srfm-setup-checklist-widget' );
704 -
705 - wp_localize_script(
706 - 'srfm-setup-checklist-widget',
707 - 'srfmSetupChecklist',
708 - [
709 - 'restUrl' => esc_url_raw( rest_url( 'sureforms/v1/dismiss-form-setup-card' ) ),
710 - 'nonce' => wp_create_nonce( 'wp_rest' ),
711 - 'formId' => $card['id'],
712 - ]
713 - );
714 -
715 - $inline_script = <<<'JS'
716 -( function () {
717 - const cfg = window.srfmSetupChecklist || {};
718 - const widget = document.getElementById( 'srfm-setup-checklist' );
719 - if ( ! widget ) {
720 - return;
721 - }
722 -
723 - const persist = function ( action ) {
724 - return fetch( cfg.restUrl, {
725 - method: 'POST',
726 - credentials: 'same-origin',
727 - keepalive: true,
728 - headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.nonce },
729 - body: JSON.stringify( { form_id: cfg.formId, action: action } ),
730 - } ).catch( function () {} );
731 - };
732 -
733 - // Beacon the CTA / view-form clicks for analytics. keepalive on the fetch lets
734 - // the request finish even though the CTA immediately navigates away.
735 - widget.addEventListener( 'click', function ( e ) {
736 - const target = e.target?.closest?.( '[data-srfm-event]' );
737 - if ( target ) {
738 - persist( target.getAttribute( 'data-srfm-event' ) );
739 - }
740 - } );
741 -}() );
742 -JS;
743 -
744 - wp_add_inline_script( 'srfm-setup-checklist-widget', $inline_script );
745 - }
746 -
747 - /**
748 - * Register the "Finish setting up" prompt as an Astra Notices admin notice (#3030).
749 - *
750 - * Hooked to admin_notices so it registers before the Astra Notices library
751 - * renders (priority 30). Shown on every admin screen EXCEPT the main dashboard,
752 - * for the newest form the current user can edit that still has an unfinished
753 - * step (default Thank You message, or no reply destination). Uses a single
754 - * stable notice id so the library's built-in ✕ dismissal is one persistent
755 - * choice ("stop nudging me"), not a per-form row.
756 - *
757 - * @since 2.12.4
758 - * @return void
759 - */
760 - public function render_thankyou_prompt_notice() {
761 - if ( ! Helper::current_user_can() || ! class_exists( 'Astra_Notices' ) ) {
762 - return;
763 - }
764 -
765 - /**
766 - * Short-circuit the "Finish setting up" Thank You notice.
767 - *
768 - * @param bool $show Whether to show the notice. Default true.
769 - *
770 - * @since 2.12.4
771 - */
772 - if ( ! apply_filters( 'srfm_show_thankyou_prompt', true ) ) {
773 - return;
774 - }
775 -
776 - // Everywhere in wp-admin except the main dashboard. A null screen fails
777 - // closed (return) rather than registering the notice on an unknown screen.
778 - $screen = get_current_screen();
779 -
780 - if ( ! $screen || 'dashboard' === $screen->id ) {
781 - return;
782 - }
783 -
784 - // A single stable notice id (not per-form): keeps both the autoloaded
785 - // `allowed_astra_notices` option and the per-user dismissal meta bounded to
786 - // one row, and lets a dismissed user short-circuit before the query runs.
787 - $notice_id = 'srfm-thankyou-prompt';
788 -
789 - // The library only checks dismissal at render (priority 30, after this
790 - // query would already have run). Check it up front so a user who dismissed
791 - // the prompt never pays for the WP_Query on subsequent admin page views.
792 - if ( 'notice-dismissed' === get_user_meta( get_current_user_id(), $notice_id, true ) ) {
793 - return;
794 - }
795 -
796 - // array_values so a filter returning a key-preserving array (e.g. the
797 - // result of array_filter()) still exposes the newest prompt at index 0.
798 - $prompts = array_values( (array) self::get_thankyou_prompt_forms() );
799 -
800 - // Validate every key build_thankyou_notice_markup() reads, not just id/edit_url
801 - // — a filter returning a partial payload would otherwise trip "Undefined array
802 - // key" warnings and esc_url( null ) deprecations on every admin page.
803 - if (
804 - empty( $prompts[0] ) || ! is_array( $prompts[0] )
805 - || empty( $prompts[0]['id'] ) || empty( $prompts[0]['edit_url'] )
806 - || empty( $prompts[0]['thankyou_url'] ) || empty( $prompts[0]['replies_url'] )
807 - || ! isset( $prompts[0]['title'] )
808 - ) {
809 - return;
810 - }
811 -
812 - $form = $prompts[0];
813 -
814 - \Astra_Notices::add_notice(
815 - [
816 - 'id' => $notice_id,
817 - 'type' => 'info',
818 - 'message' => self::build_thankyou_notice_markup( $form ),
819 - 'class' => 'srfm-thankyou-notice',
820 - 'is_dismissible' => true,
821 - 'display-with-other-notices' => true,
822 - // Render late so this nudge never pre-empts higher-priority notices
823 - // (e.g. Astra's minimum-version warnings, which are display-with-
824 - // other-notices => false and would be skipped once ours renders).
825 - 'priority' => 100,
826 - ]
827 - );
828 -
829 - // The message is wp_kses_post'd by the library, so the brand-orange styling
830 - // is printed through the notice's pre-markup hook instead of inline.
831 - add_action( 'astra_notice_before_markup_' . $notice_id, [ $this, 'print_thankyou_notice_styles' ] );
832 -
833 - // Track clicks on the CTAs and the dismiss ✕ via the shared notice-response
834 - // endpoint, enqueued only when the notice actually renders.
835 - add_action( 'astra_notice_after_markup_' . $notice_id, [ $this, 'enqueue_thankyou_notice_tracking' ] );
836 - }
837 -
838 - /**
839 - * Enqueue the click-tracking for the Thank You notice (#3030).
840 - *
841 - * Sends an analytics beacon to the shared `srfm_notice_response` AJAX handler
842 - * when a CTA or the dismiss ✕ is clicked. Uses `keepalive` so the beacon
843 - * survives the navigation the CTA links trigger.
844 - *
845 - * @since 2.12.4
846 - * @return void
847 - */
848 - public function enqueue_thankyou_notice_tracking() {
849 - if ( wp_script_is( 'srfm-thankyou-notice-track', 'enqueued' ) ) {
850 - return;
851 - }
852 -
853 - wp_register_script( 'srfm-thankyou-notice-track', '', [], SRFM_VER, true );
854 - wp_enqueue_script( 'srfm-thankyou-notice-track' );
855 -
856 - $config = wp_json_encode(
857 - [
858 - 'ajaxurl' => admin_url( 'admin-ajax.php' ),
859 - 'nonce' => wp_create_nonce( 'srfm_notice_response' ),
860 - ]
861 - );
862 -
863 - wp_add_inline_script( 'srfm-thankyou-notice-track', 'window.srfmThankYouNoticeTrack = ' . $config . ';', 'before' );
864 -
865 - $inline_script = <<<'JS'
866 -( function () {
867 - const cfg = window.srfmThankYouNoticeTrack || {};
868 - const wrap = document.querySelector( '.srfm-thankyou-notice' );
869 - if ( ! wrap ) {
870 - return;
871 - }
872 - const noticeId = wrap.id || '';
873 - const send = function ( button ) {
874 - const body = new URLSearchParams();
875 - body.append( 'action', 'srfm_notice_response' );
876 - body.append( 'nonce', cfg.nonce );
877 - body.append( 'notice_id', noticeId );
878 - body.append( 'button', button );
879 - fetch( cfg.ajaxurl, {
880 - method: 'POST',
881 - credentials: 'same-origin',
882 - keepalive: true,
883 - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
884 - body: body.toString(),
885 - } ).catch( function () {} );
886 - };
887 - // Delegate from the wrapper: this inline script runs at parse time, before
888 - // core's common.js injects the .notice-dismiss ✕ (on DOMContentLoaded), so a
889 - // direct querySelector for it would find nothing and the dismiss beacon would
890 - // never fire. Delegation catches the ✕ and the CTAs whenever they exist.
891 - const ctas = [
892 - [ '.srfm-ty-edit-form', 'edit_form' ],
893 - [ '.srfm-ty-set-replies', 'set_replies' ],
894 - [ '.srfm-ty-edit-thankyou', 'edit_thankyou' ],
895 - ];
896 - wrap.addEventListener( 'click', function ( e ) {
897 - if ( e.target.closest( '.notice-dismiss' ) ) {
898 - send( 'dismissed' );
899 - return;
900 - }
901 - for ( let i = 0; i < ctas.length; i++ ) {
902 - if ( e.target.closest( ctas[ i ][ 0 ] ) ) {
903 - send( ctas[ i ][ 1 ] );
904 - return;
905 - }
906 - }
907 - } );
908 -}() );
909 -JS;
910 -
911 - wp_add_inline_script( 'srfm-thankyou-notice-track', $inline_script );
912 - }
913 -
914 - /**
915 - * Print the Thank You notice's brand-orange styling (#3030).
916 - *
917 - * Fired via astra_notice_before_markup_{id} so it lands right before the notice
918 - * and only when the notice actually renders.
919 - *
920 - * @since 2.12.4
921 - * @return void
922 - */
923 - public function print_thankyou_notice_styles() {
924 - // The library wp_kses_post()'s the message, which strips <svg> and data:
925 - // image srcs, so the SureForms mark is painted as a CSS background here
926 - // (this hook fires outside that kses call). URL-encoded, not base64, so the
927 - // value is fully percent-encoded and safe to pass through esc_url.
928 - $icon = 'data:image/svg+xml,' . rawurlencode(
929 - '<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 32 32"><path fill="#D54407" fill-rule="evenodd" clip-rule="evenodd" d="M32 0H0V32H32V0ZM22.8573 6.85728H9.14304V11.4287V13.7144L11.4288 11.4287H22.8573V6.85728ZM20.5717 13.7146H9.14314V18.286V20.5714V20.5718V25.1428H16.0003V20.5714H9.14351L11.4289 18.286H20.5717V13.7146Z"/></svg>'
930 - );
931 - ?>
932 - <style id="srfm-thankyou-notice-styles">
933 - .srfm-thankyou-notice.notice { border-left-color: #D54407; }
934 - /* Stack our blocks (the library lays the container out as a flex row) and reserve room on the left for the SureForms mark. */
935 - .srfm-thankyou-notice .astra-notice-container { display: block; padding: 4px 0 4px 52px; background: url('<?php echo esc_url( $icon, [ 'data' ] ); ?>') no-repeat 4px 6px; background-size: 32px 32px; }
936 - .srfm-thankyou-notice .srfm-thankyou-notice__title { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: #1d2327; }
937 - .srfm-thankyou-notice .srfm-thankyou-notice__text { margin: 0 0 10px; color: #50575e; }
938 - .srfm-thankyou-notice .srfm-thankyou-notice__actions { margin: 12px 0 2px; display: flex; flex-wrap: wrap; gap: 10px 20px; align-items: center; }
939 - .srfm-thankyou-notice .button-primary { background: #D54407; border-color: #D54407; color: #fff; box-shadow: none; text-shadow: none; }
940 - .srfm-thankyou-notice .button-primary:hover, .srfm-thankyou-notice .button-primary:focus { background: #C83B00; border-color: #C83B00; color: #fff; box-shadow: none; }
941 - .srfm-thankyou-notice .button:not(.button-primary) { background: transparent; border-color: transparent; color: #D54407; box-shadow: none; padding: 0; }
942 - .srfm-thankyou-notice .button:not(.button-primary):hover, .srfm-thankyou-notice .button:not(.button-primary):focus { background: transparent; border-color: transparent; color: #C83B00; box-shadow: none; }
943 - .srfm-thankyou-notice .button-primary:focus { outline: 2px solid #D54407; outline-offset: 1px; }
944 - </style>
945 - <?php
946 - }
947 -
948 - /**
949 - * Check and save the first form creation time stamp.
950 - * If not already saved.
951 - *
952 - * @since 1.10.1
953 - * @return void
954 - */
955 - public static function save_first_form_creation_time_stamp() {
956 - if ( ! Helper::current_user_can() || self::is_first_form_created() || ! defined( 'SRFM_FORMS_POST_TYPE' ) || ! post_type_exists( SRFM_FORMS_POST_TYPE ) ) {
957 - return;
958 - }
959 -
960 - // Get the first form creation time from the database that is published.
961 - $query = new \WP_Query(
962 - [
963 - 'post_type' => SRFM_FORMS_POST_TYPE,
964 - 'posts_per_page' => 1,
965 - 'orderby' => 'date',
966 - 'order' => 'ASC',
967 - 'fields' => 'ids',
968 - 'post_status' => 'publish',
969 - ]
970 - );
971 -
972 - if ( ! empty( $query->posts ) && isset( $query->posts[0] ) ) {
973 - // Get the first post from the query result.
974 - $post_id = $query->posts[0];
975 - // Get the post creation time in GMT.
976 - $creation_time = get_post_field( 'post_date_gmt', $post_id );
977 - // Convert the creation time to a timestamp.
978 - $timestamp = strtotime( $creation_time );
979 -
980 - if ( ! $timestamp ) {
981 - return;
982 - }
983 -
984 - Helper::update_srfm_option( 'first_form_created_at', $timestamp );
985 - }
986 - }
987 -
988 - /**
989 - * Check if n days have passed since the first form creation.
990 - * This is used to determine if the dynamic nudges should be shown.
991 - *
992 - * @param int $days Number of days to check.
993 - * @since 1.10.1
994 - * @return bool
995 - */
996 - public static function check_first_form_creation_threshold( $days = 3 ) {
997 - $first_form_creation_time_stamp = self::get_first_form_creation_time_stamp();
998 -
999 - if ( ! $first_form_creation_time_stamp ) {
1000 - return false; // No forms created yet.
1001 - }
1002 -
1003 - /**
1004 - * Calculate the number of days since the first form was created.
1005 - */
1006 - $days_from_creation = ( strtotime( current_time( 'mysql' ) ) - $first_form_creation_time_stamp ) / DAY_IN_SECONDS;
1007 -
1008 - // Return a boolean indicating if the number of days since creation is greater than the specified days.
1009 - return $days_from_creation > $days;
1010 - }
1011 -
1012 - /**
1013 73 * Show action on plugin page.
1014 74 *
1015 75 * @param array $links links.
1016 76 * @return array
@@ -1016,19 +76,17 @@
1016 76 * @return array
1017 77 * @since 1.4.2
1018 78 */
1019 79 public function add_action_links( $links ) {
1020 - if ( ! Helper::has_pro() ) {
80 + if ( ! defined( 'SRFM_PRO_FILE' ) && ! file_exists( WP_PLUGIN_DIR . '/sureforms-pro/sureforms-pro.php' ) ) {
1021 81 // Display upsell link if SureForms Pro is not installed.
1022 - $upsell_link = Helper::get_sureforms_website_url( 'pricing', [ 'utm_medium' => 'plugin-list' ] );
1023 -
1024 - ob_start();
1025 - ?>
1026 - <a href="<?php echo esc_url( $upsell_link ); ?>" target="_blank" rel="noreferrer" class="sureforms-plugins-go-pro">
1027 - <?php echo esc_html__( 'Get SureForms Pro', 'sureforms' ); ?>
1028 - </a>
1029 - <?php
1030 - $links[] = trim( ob_get_clean() );
82 + $upsell_link = add_query_arg(
83 + [
84 + 'utm_medium' => 'plugin-list',
85 + ],
86 + Helper::get_sureforms_website_url( 'pricing' )
87 + );
88 + $links[] = '<a href="' . esc_url( $upsell_link ) . '" target="_blank" rel="noreferrer" class="sureforms-plugins-go-pro">' . esc_html__( 'Get SureForms Pro', 'sureforms' ) . '</a>';
1031 89 }
1032 90
1033 91 return $links;
1034 92 }
@@ -1096,15 +154,20 @@
1096 154 * @return void
1097 155 * @since 0.0.1
1098 156 */
1099 157 public function add_menu_page() {
1100 - $menu_slug = 'sureforms_menu';
158 + if ( ! current_user_can( 'manage_options' ) ) {
159 + return;
160 + }
1101 161
162 + $capability = 'manage_options';
163 + $menu_slug = 'sureforms_menu';
164 +
1102 165 $logo = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/icon.svg' );
1103 166 add_menu_page(
1104 167 __( 'SureForms', 'sureforms' ),
1105 168 __( 'SureForms', 'sureforms' ),
1106 - self::$sureforms_page_default_capability,
169 + 'edit_others_posts',
1107 170 $menu_slug,
1108 171 static function () {
1109 172 },
1110 173 'data:image/svg+xml;base64,' . base64_encode( $logo ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
@@ -1115,9 +178,9 @@
1115 178 add_submenu_page(
1116 179 $menu_slug,
1117 180 __( 'Dashboard', 'sureforms' ),
1118 181 __( 'Dashboard', 'sureforms' ),
1119 - self::$sureforms_page_default_capability,
182 + $capability,
1120 183 $menu_slug,
1121 184 [ $this, 'render_dashboard' ]
1122 185 );
1123 186 }
@@ -1133,9 +196,9 @@
1133 196 add_submenu_page(
1134 197 'sureforms_menu',
1135 198 __( 'Settings', 'sureforms' ),
1136 199 __( 'Settings', 'sureforms' ),
1137 - self::$sureforms_page_default_capability,
200 + 'edit_others_posts',
1138 201 'sureforms_form_settings',
1139 202 $callback
1140 203 );
1141 204
@@ -1148,190 +211,8 @@
1148 211 }
1149 212 }
1150 213
1151 214 /**
1152 - * Open to Upgrade to Pro submenu link in new tab.
1153 - *
1154 - * @return void
1155 - * @since 1.6.1
1156 - */
1157 - public function add_upgrade_to_pro_target_attr() {
1158 - ?>
1159 - <script type="text/javascript">
1160 - document.addEventListener('DOMContentLoaded', function () {
1161 - // Upgrade link handler.
1162 - // IMPORTANT: If this URL changes, also update it in the `add_upgrade_to_pro` function.
1163 - const upgradeLink = document.querySelector('a[href*="https://sureforms.com/upgrade"]');
1164 - if (upgradeLink) {
1165 - upgradeLink.addEventListener('click', e => {
1166 - e.preventDefault();
1167 - window.open(upgradeLink.href, '_blank');
1168 - });
1169 - }
1170 - });
1171 - </script>
1172 - <?php
1173 - }
1174 -
1175 - /**
1176 - * Add Upgrade to pro menu item.
1177 - *
1178 - * @return void
1179 - * @since 1.6.1
1180 - */
1181 - public function add_upgrade_to_pro() {
1182 - // The url used here is used as a selector for css to style the upgrade to pro submenu.
1183 - // If you are changing this url, please make sure to update the css as well.
1184 - $upgrade_url = Helper::get_sureforms_website_url( 'upgrade', [ 'utm_medium' => 'submenu_link_upgrade' ] );
1185 -
1186 - add_submenu_page(
1187 - 'sureforms_menu',
1188 - __( 'Upgrade', 'sureforms' ),
1189 - __( 'Upgrade', 'sureforms' ),
1190 - self::$sureforms_page_default_capability,
1191 - $upgrade_url
1192 - );
1193 - }
1194 -
1195 - /**
1196 - * Add Quiz empty state submenu page for free users.
1197 - *
1198 - * @return void
1199 - * @since 2.7.0
1200 - */
1201 - public function add_quiz_page() {
1202 - add_submenu_page(
1203 - 'sureforms_menu',
1204 - __( 'Quiz Entries', 'sureforms' ),
1205 - __( 'Quizzes', 'sureforms' ) .
1206 - ' <span style="color:#4ADE80;font-size:9px;font-weight:600;">' .
1207 - esc_html__( 'New', 'sureforms' ) .
1208 - '</span>',
1209 - self::$sureforms_page_default_capability,
1210 - 'sureforms_quiz_entries',
1211 - [ $this, 'render_quiz_empty_state' ],
1212 - 5
1213 - );
1214 - }
1215 -
1216 - /**
1217 - * Quiz empty state page callback.
1218 - *
1219 - * @return void
1220 - * @since 2.7.0
1221 - */
1222 - public function render_quiz_empty_state() {
1223 - ?>
1224 - <div id="srfm-quiz-entries-root" class="srfm-admin-wrapper"></div>
1225 - <?php
1226 - }
1227 -
1228 - /**
1229 - * Add Survey Reports promotional submenu page for free users.
1230 - *
1231 - * @return void
1232 - * @since 2.8.0
1233 - */
1234 - public function add_survey_reports_page() {
1235 - add_submenu_page(
1236 - 'sureforms_menu',
1237 - __( 'Survey Reports', 'sureforms' ),
1238 - __( 'Survey Reports', 'sureforms' ) .
1239 - ' <span style="color:#4ADE80;font-size:9px;font-weight:600;">' .
1240 - esc_html__( 'New', 'sureforms' ) .
1241 - '</span>',
1242 - self::$sureforms_page_default_capability,
1243 - 'sureforms_survey_reports',
1244 - [ $this, 'render_survey_empty_state' ],
1245 - 6
1246 - );
1247 - }
1248 -
1249 - /**
1250 - * Survey empty state page callback.
1251 - *
1252 - * @return void
1253 - * @since 2.8.0
1254 - */
1255 - public function render_survey_empty_state() {
1256 - ?>
1257 - <div id="srfm-survey-empty-state-root" class="srfm-admin-wrapper"></div>
1258 - <?php
1259 - }
1260 -
1261 - /**
1262 - * Add Partial Entries promotional submenu page for free users.
1263 - *
1264 - * @return void
1265 - * @since 2.9.0
1266 - */
1267 - public function add_partial_entries_page() {
1268 - add_submenu_page(
1269 - 'sureforms_menu',
1270 - __( 'Partial Entries', 'sureforms' ),
1271 - __( 'Partial Entries', 'sureforms' ) .
1272 - ' <span style="color:#4ADE80;font-size:9px;font-weight:600;">' .
1273 - esc_html__( 'New', 'sureforms' ) .
1274 - '</span>',
1275 - self::$sureforms_page_default_capability,
1276 - 'sureforms_partial_entries',
1277 - [ $this, 'render_partial_entries_empty_state' ],
1278 - 7
1279 - );
1280 - }
1281 -
1282 - /**
1283 - * Partial Entries empty state page callback.
1284 - *
1285 - * @return void
1286 - * @since 2.9.0
1287 - */
1288 - public function render_partial_entries_empty_state() {
1289 - ?>
1290 - <div id="srfm-partial-entries-empty-state-root" class="srfm-admin-wrapper"></div>
1291 - <?php
1292 - }
1293 -
1294 - /**
1295 - * Add SMTP promotional submenu page.
1296 - *
1297 - * @return void
1298 - * @since 1.7.1
1299 - */
1300 - public function add_suremail_page() {
1301 - add_submenu_page(
1302 - 'sureforms_menu',
1303 - __( 'SMTP', 'sureforms' ),
1304 - __( 'SMTP', 'sureforms' ),
1305 - self::$sureforms_page_default_capability,
1306 - 'sureforms_smtp',
1307 - [ $this, 'suremail_page_callback' ]
1308 - );
1309 -
1310 - // Get the current submenu page.
1311 - $submenu_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- $_GET['page'] does not provide nonce.
1312 -
1313 - // Check if SureMail is installed and active.
1314 - if ( 'sureforms_smtp' === $submenu_page && file_exists( WP_PLUGIN_DIR . '/suremails/suremails.php' ) && is_plugin_active( 'suremails/suremails.php' ) ) {
1315 - // Plugin is installed and active - redirect to SureMail dashboard.
1316 - wp_safe_redirect( admin_url( 'options-general.php?page=suremail#/dashboard' ) );
1317 - exit;
1318 - }
1319 - }
1320 -
1321 - /**
1322 - * SMTP promotional page callback.
1323 - *
1324 - * @return void
1325 - * @since 1.7.1
1326 - */
1327 - public function suremail_page_callback() {
1328 - ?>
1329 - <div id="srfm-suremail-container" class="srfm-admin-wrapper"></div>
1330 - <?php
1331 - }
1332 -
1333 - /**
1334 215 * Render Admin Dashboard.
1335 216 *
1336 217 * @return void
1337 218 * @since 0.0.1
@@ -1336,11 +217,9 @@
1336 217 * @return void
1337 218 * @since 0.0.1
1338 219 */
1339 220 public function render_dashboard() {
1340 - ?>
1341 - <div id="srfm-dashboard-container" class="srfm-admin-wrapper"></div>
1342 - <?php
221 + echo '<div id="srfm-dashboard-container"></div>';
1343 222 }
1344 223
1345 224 /**
1346 225 * Settings page callback.
@@ -1348,43 +227,12 @@
1348 227 * @return void
1349 228 * @since 0.0.1
1350 229 */
1351 230 public function settings_page_callback() {
1352 - ?>
1353 - <div id="srfm-settings-container" class="srfm-admin-wrapper"></div>
1354 - <?php
231 + echo '<div id="srfm-settings-container"></div>';
1355 232 }
1356 233
1357 234 /**
1358 - * Add Learn submenu page.
1359 - *
1360 - * @return void
1361 - * @since 2.5.2
1362 - */
1363 - public function add_learn_page() {
1364 - add_submenu_page(
1365 - 'sureforms_menu',
1366 - __( 'Learn', 'sureforms' ),
1367 - __( 'Learn', 'sureforms' ),
1368 - self::$sureforms_page_default_capability,
1369 - 'sureforms_learn',
1370 - [ $this, 'render_learn' ]
1371 - );
1372 - }
1373 -
1374 - /**
1375 - * Learn page callback.
1376 - *
1377 - * @return void
1378 - * @since 2.5.2
1379 - */
1380 - public function render_learn() {
1381 - ?>
1382 - <div id="srfm-learn-root" class="srfm-admin-wrapper"></div>
1383 - <?php
1384 - }
1385 -
1386 - /**
1387 235 * Add new form menu item.
1388 236 *
1389 237 * @return void
1390 238 * @since 0.0.1
@@ -1391,62 +239,27 @@
1391 239 */
1392 240 public function add_new_form() {
1393 241 add_submenu_page(
1394 242 'sureforms_menu',
1395 - __( 'Forms', 'sureforms' ),
1396 - __( 'Forms', 'sureforms' ),
1397 - self::$sureforms_page_default_capability,
1398 - 'sureforms_forms',
1399 - [ $this, 'render_forms' ],
1400 - 1
1401 - );
1402 - add_submenu_page(
1403 - 'sureforms_menu',
1404 243 __( 'New Form', 'sureforms' ),
1405 244 __( 'New Form', 'sureforms' ),
1406 - self::$sureforms_page_default_capability,
245 + 'edit_others_posts',
1407 246 'add-new-form',
1408 247 [ $this, 'add_new_form_callback' ],
1409 248 2
1410 249 );
1411 - $entries_hook = add_submenu_page(
250 + add_submenu_page(
1412 251 'sureforms_menu',
1413 252 __( 'Entries', 'sureforms' ),
1414 253 __( 'Entries', 'sureforms' ),
1415 - self::$sureforms_page_default_capability,
254 + 'edit_others_posts',
1416 255 SRFM_ENTRIES,
1417 256 [ $this, 'render_entries' ],
1418 257 3
1419 258 );
1420 -
1421 - add_submenu_page(
1422 - 'sureforms_menu',
1423 - __( 'Payments', 'sureforms' ),
1424 - __( 'Payments', 'sureforms' ),
1425 - self::$sureforms_page_default_capability,
1426 - SRFM_PAYMENTS,
1427 - [ $this, 'render_payments' ],
1428 - 4
1429 - );
1430 -
1431 - if ( $entries_hook ) {
1432 - add_action( 'load-' . $entries_hook, [ $this, 'mark_entries_page_visit' ] );
1433 - }
1434 259 }
1435 260
1436 261 /**
1437 - * Payments page callback.
1438 - *
1439 - * @return void
1440 - * @since 2.0.0
1441 - */
1442 - public function render_payments() {
1443 - ?>
1444 - <div id="srfm-payments-react-container" class="srfm-admin-wrapper"></div>
1445 - <?php
1446 - }
1447 -
1448 - /**
1449 262 * Add new form mentu item callback.
1450 263 *
1451 264 * @return void
1452 265 * @since 0.0.1
@@ -1451,109 +264,44 @@
1451 264 * @return void
1452 265 * @since 0.0.1
1453 266 */
1454 267 public function add_new_form_callback() {
1455 - ?>
1456 - <div id="srfm-add-new-form-container" class="srfm-admin-wrapper"></div>
1457 - <?php
268 + echo '<div id="srfm-add-new-form-container"></div>';
1458 269 }
1459 270
1460 271 /**
1461 - * Forms page callback.
1462 - *
1463 - * @return void
1464 - * @since 2.0.0
1465 - */
1466 - public function render_forms() {
1467 - ?>
1468 - <div id="srfm-forms-root" class="srfm-admin-wrapper"></div>
1469 - <?php
1470 - }
1471 -
1472 - /**
1473 272 * Entries page callback.
1474 273 *
1475 274 * @since 0.0.13
1476 - * @since 2.0.0 - Updated the entries UI and the function definition.
1477 275 * @return void
1478 276 */
1479 277 public function render_entries() {
1480 - echo '<div id="srfm-entries-root"></div>';
1481 - }
1482 -
1483 - /**
1484 - * Add notification badge to SureForms menu when there are new entries.
1485 - *
1486 - * @since 1.7.3
1487 - * @return void
1488 - */
1489 - public function maybe_add_entries_badge() {
1490 - if ( ! Helper::current_user_can() ) {
278 + // Render single entry view.
279 + // Adding the phpcs ignore nonce verification as no database operations are performed in this function, it is used to display the single entry view.
280 + if ( isset( $_GET['entry_id'] ) && is_numeric( $_GET['entry_id'] ) && isset( $_GET['view'] ) && 'details' === $_GET['view'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not needed here and explained in the comments above as well.
281 + $single_entry_view = new Single_Entry();
282 + $single_entry_view->render();
1491 283 return;
1492 284 }
1493 285
1494 - // If currently viewing the entries listing page, mark it as visited and skip the badge.
1495 - if ( isset( $_GET['page'] ) && SRFM_ENTRIES === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only checking the page slug.
1496 - $this->mark_entries_page_visit();
286 + // Render all entries view.
287 + $entries_table = new Entries_List_Table();
288 + $entries_table->prepare_items();
289 + echo '<div class="wrap"><h1 class="wp-heading-inline">' . esc_html__( 'Entries', 'sureforms' ) . '</h1>';
290 + if ( empty( $entries_table->all_entries_count ) && empty( $entries_table->trash_entries_count ) ) {
291 + $instance = Post_Types::get_instance();
292 + $instance->sureforms_render_blank_state( SRFM_ENTRIES );
293 + $instance->get_blank_state_styles();
1497 294 return;
1498 295 }
1499 -
1500 - $srfm_options = get_option( 'srfm_options', [] );
1501 - $last_visit = isset( $srfm_options['entries_last_visited'] ) ? absint( $srfm_options['entries_last_visited'] ) : 0;
1502 - $new_entries = Entries::get_entries_count_after( $last_visit );
1503 -
1504 - if ( $new_entries <= 0 ) {
1505 - return;
1506 - }
1507 -
1508 - global $menu;
1509 - foreach ( $menu as $index => $item ) {
1510 - if ( isset( $item[2] ) && 'sureforms_menu' === $item[2] ) {
1511 - ob_start();
1512 - ?>
1513 - <span class="srfm-update-dot"></span>
1514 - <?php
1515 - $dot_html = ob_get_clean();
1516 - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Adding notifications for menu item.
1517 - $menu[ $index ][0] .= $dot_html;
1518 - break;
1519 - }
1520 - }
1521 -
1522 - global $submenu;
1523 - if ( isset( $submenu['sureforms_menu'] ) ) {
1524 - foreach ( $submenu['sureforms_menu'] as $index => $sub_item ) {
1525 - if ( isset( $sub_item[2] ) && SRFM_ENTRIES === $sub_item[2] ) {
1526 - ob_start();
1527 - ?>
1528 - <span class="update-plugins count-<?php echo absint( $new_entries ); ?>">
1529 - <span class="plugin-count"><?php echo absint( $new_entries ); ?></span>
1530 - </span>
1531 - <?php
1532 - $badge_html = ob_get_clean();
1533 - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Adding notifications for submenu item.
1534 - $submenu['sureforms_menu'][ $index ][0] .= $badge_html;
1535 - break;
1536 - }
1537 - }
1538 - }
296 + echo '<form method="get">';
297 + echo '<input type="hidden" name="page" value="sureforms_entries">';
298 + $entries_table->display();
299 + echo '</form>';
300 + echo '</div>';
1539 301 }
1540 302
1541 303 /**
1542 - * Mark the user's visit to the entries page.
1543 - *
1544 - * @since 1.7.3
1545 - * @return void
1546 - */
1547 - public function mark_entries_page_visit() {
1548 - if ( Helper::current_user_can() ) {
1549 - $srfm_options = get_option( 'srfm_options', [] );
1550 - $srfm_options['entries_last_visited'] = time();
1551 - \SRFM\Inc\Helper::update_admin_settings_option( 'srfm_options', $srfm_options );
1552 - }
1553 - }
1554 -
1555 - /**
1556 304 * Adds a settings link to the plugin action links on the plugins page.
1557 305 *
1558 306 * @param array $links An array of plugin action links.
1559 307 * @param string $file The plugin file path.
@@ -1561,22 +309,15 @@
1561 309 * @since 0.0.1
1562 310 */
1563 311 public function add_settings_link( $links, $file ) {
1564 312 if ( 'sureforms/sureforms.php' === $file ) {
1565 - ob_start();
1566 - ?>
1567 - <a href="<?php echo esc_url( admin_url( 'admin.php?page=sureforms_form_settings&tab=general-settings' ) ); ?>">
1568 - <?php echo esc_html__( 'Settings', 'sureforms' ); ?>
1569 - </a>
1570 - <?php
1571 - $settings_link_html = ob_get_clean();
1572 - $plugin_links = apply_filters(
313 + $plugin_links = apply_filters(
1573 314 'sureforms_plugin_action_links',
1574 315 [
1575 - 'sureforms_settings' => $settings_link_html,
316 + 'sureforms_settings' => '<a href="' . esc_url( admin_url( 'admin.php?page=sureforms_form_settings&tab=general-settings' ) ) . '">' . esc_html__( 'Settings', 'sureforms' ) . '</a>',
1576 317 ]
1577 318 );
1578 - $links = array_merge( $plugin_links, $links );
319 + $links = array_merge( $plugin_links, $links );
1579 320 }
1580 321 return $links;
1581 322 }
1582 323
@@ -1606,9 +347,8 @@
1606 347 wp_enqueue_style( SRFM_SLUG . '-backend-blocks', $css_uri . 'blocks/default/backend' . $file_prefix . '.css', [], SRFM_VER );
1607 348 wp_enqueue_style( SRFM_SLUG . '-intl', $vendor_css_uri . 'intl/intlTelInput-backend.min.css', [], SRFM_VER );
1608 349 wp_enqueue_style( SRFM_SLUG . '-common', $css_uri . 'common' . $file_prefix . '.css', [], SRFM_VER );
1609 350 wp_enqueue_style( SRFM_SLUG . '-reactQuill', $vendor_css_uri . 'quill/quill.snow.css', [], SRFM_VER );
1610 - wp_add_inline_style( SRFM_SLUG . '-reactQuill', self::QUILL_1X_INLINE_CSS );
1611 351 wp_enqueue_style( SRFM_SLUG . '-single-form-modal', $css_uri . 'single-form-setting' . $file_prefix . '.css', [], SRFM_VER );
1612 352
1613 353 // if version is equal to or lower than 6.6.2 then add compatibility css.
1614 354 if ( version_compare( $wp_version, '6.6.2', '<=' ) ) {
@@ -1687,8 +427,9 @@
1687 427 global $wp_version;
1688 428
1689 429 $file_prefix = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? '' : '.min';
1690 430 $dir_name = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? 'unminified' : 'minified';
431 + $js_uri = SRFM_URL . 'assets/js/' . $dir_name . '/';
1691 432 $css_uri = SRFM_URL . 'assets/css/' . $dir_name . '/';
1692 433 $is_rtl = is_rtl();
1693 434 $rtl = $is_rtl ? '-rtl' : '';
1694 435
@@ -1695,135 +436,30 @@
1695 436 /**
1696 437 * List of the handles in which we need to add translation compatibility.
1697 438 */
1698 439 $script_translations_handlers = [];
1699 - $onboarding_instance = Onboarding::get_instance();
1700 - $current_user = wp_get_current_user();
1701 440
1702 441 $localization_data = [
1703 - 'site_url' => get_site_url(),
1704 - 'current_user_login' => $current_user->user_login ?? '',
1705 - 'website_lead_details' => [
1706 - 'first_name' => $current_user->first_name ?? '',
1707 - 'last_name' => $current_user->last_name ?? '',
1708 - 'email' => $current_user->user_email ?? '',
1709 - ],
1710 - 'breadcrumbs' => $this->get_breadcrumbs_for_current_page(),
1711 - 'sureforms_dashboard_url' => admin_url( '/admin.php?page=sureforms_menu' ),
1712 - 'plugin_version' => SRFM_VER,
1713 - 'global_settings_nonce' => Helper::current_user_can() ? wp_create_nonce( 'wp_rest' ) : '',
1714 - 'is_pro_active' => Helper::has_pro(),
1715 - 'is_first_form_created' => self::is_first_form_created(),
1716 - 'check_three_days_threshold' => self::check_first_form_creation_threshold(),
1717 - 'check_eight_days_threshold' => self::check_first_form_creation_threshold( 8 ),
1718 - 'pro_plugin_version' => Helper::has_pro() ? SRFM_PRO_VER : '',
1719 - 'pro_plugin_name' => Helper::has_pro() && defined( 'SRFM_PRO_PRODUCT' ) ? SRFM_PRO_PRODUCT : 'SureForms Pro',
1720 - 'sureforms_pricing_page' => Helper::get_sureforms_website_url( 'pricing' ),
1721 - 'field_spacing_vars' => Helper::get_css_vars(),
1722 - 'is_ver_lower_than_6_7' => version_compare( $wp_version, '6.6.2', '<=' ),
1723 - 'integrations' => Helper::sureforms_get_integration(),
1724 - 'rotating_plugin_banner' => Helper::get_rotating_plugin_banner(),
1725 - 'ajax_url' => admin_url( 'admin-ajax.php' ),
1726 - 'sf_plugin_manager_nonce' => wp_create_nonce( 'sf_plugin_manager_nonce' ),
1727 - 'plugin_installer_nonce' => wp_create_nonce( 'updates' ),
1728 - 'plugin_activating_text' => __( 'Activating...', 'sureforms' ),
1729 - 'plugin_activated_text' => __( 'Activated', 'sureforms' ),
1730 - 'plugin_activate_text' => __( 'Activate', 'sureforms' ),
1731 - 'plugin_installing_text' => __( 'Installing...', 'sureforms' ),
1732 - 'plugin_installed_text' => __( 'Installed', 'sureforms' ),
1733 - 'privacy_policy_url' => Helper::get_sureforms_website_url( 'privacy-policy/' ),
1734 - 'is_rtl' => $is_rtl,
1735 - 'onboarding_completed' => method_exists( $onboarding_instance, 'get_onboarding_status' ) ? $onboarding_instance->get_onboarding_status() : false,
1736 - 'migration_banner_dismissed' => method_exists( $onboarding_instance, 'is_migration_banner_dismissed' ) ? $onboarding_instance->is_migration_banner_dismissed() : false,
1737 - 'migration_settings_url' => admin_url( 'admin.php?page=sureforms_form_settings&tab=migration-settings' ),
1738 - 'onboarding_redirect' => isset( $_GET['srfm-activation-redirect'] ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required for the activation redirection.
1739 - 'pointer_nonce' => wp_create_nonce( 'sureforms_pointer_action' ),
1740 - 'general_settings_url' => admin_url( '/options-general.php' ),
1741 - 'additional_header_nav_items' => [],
1742 - // Smart tags for the Global Defaults email notification fields.
1743 - // srfm_block_data is only available in the block editor; these keys
1744 - // make the same data accessible on the settings page via srfm_admin.
1745 - 'smart_tags_array' => Smart_Tags::smart_tag_list(),
1746 - 'smart_tags_array_email' => Smart_Tags::email_smart_tag_list(),
1747 - // Default confirmation message HTML (icon + heading + text) used as
1748 - // the initial React state before the settings API response arrives.
1749 - 'default_confirmation_message' => Global_Settings::get_default_confirmation_message(),
1750 - 'payments' => apply_filters(
1751 - 'srfm_admin_localize_payments_data',
1752 - [
1753 - 'stripe_connected' => Stripe_Helper::is_stripe_connected(),
1754 - 'stripe_mode' => Stripe_Helper::get_stripe_mode(),
1755 - 'stripe_connect_url' => Stripe_Helper::get_stripe_settings_url(),
1756 - 'currencies_data' => Payment_Helper::get_all_currencies_data(),
1757 - 'zero_decimal_currencies' => Payment_Helper::get_zero_decimal_currencies(),
1758 - 'webhook_url' => Stripe_Helper::get_webhook_url(),
1759 - 'webhook_test_connected' => Stripe_Helper::is_webhook_configured( 'test', true ),
1760 - 'webhook_live_connected' => Stripe_Helper::is_webhook_configured( 'live', true ),
1761 - 'is_transaction_present' => Stripe_Helper::is_transaction_present(),
1762 - 'payment_currency' => Payment_Helper::get_currency(),
1763 - 'currency_sign_position' => Payment_Helper::get_currency_sign_position(),
1764 - ]
1765 - ),
1766 - 'mcp_adapter_status' => file_exists( WP_PLUGIN_DIR . '/mcp-adapter/mcp-adapter.php' )
1767 - ? ( is_plugin_active( 'mcp-adapter/mcp-adapter.php' ) ? 'active' : 'installed' )
1768 - : 'not_installed',
1769 - 'mcp_endpoint_url' => esc_url_raw( rest_url( 'sureforms/v1/mcp' ) ),
442 + 'site_url' => get_site_url(),
443 + 'breadcrumbs' => $this->get_breadcrumbs_for_current_page(),
444 + 'sureforms_dashboard_url' => admin_url( '/admin.php?page=sureforms_menu' ),
445 + 'plugin_version' => SRFM_VER,
446 + 'global_settings_nonce' => current_user_can( 'manage_options' ) ? wp_create_nonce( 'wp_rest' ) : '',
447 + 'is_pro_active' => defined( 'SRFM_PRO_VER' ),
448 + 'pro_plugin_version' => defined( 'SRFM_PRO_VER' ) ? SRFM_PRO_VER : '',
449 + 'pro_plugin_name' => defined( 'SRFM_PRO_VER' ) && defined( 'SRFM_PRO_PRODUCT' ) ? SRFM_PRO_PRODUCT : 'SureForms Pro',
450 + 'sureforms_pricing_page' => Helper::get_sureforms_website_url( 'pricing' ),
451 + 'field_spacing_vars' => Helper::get_css_vars(),
452 + 'is_ver_lower_than_6_7' => version_compare( $wp_version, '6.6.2', '<=' ),
1770 453 ];
1771 454
1772 - $is_screen_sureforms_menu = Helper::validate_request_context( 'sureforms_menu', 'page' );
1773 - $is_screen_add_new_form = Helper::validate_request_context( 'add-new-form', 'page' );
1774 - $is_screen_sureforms_forms = Helper::validate_request_context( 'sureforms_forms', 'page' );
1775 - $is_screen_sureforms_form_settings = Helper::validate_request_context( 'sureforms_form_settings', 'page' );
1776 - $is_screen_sureforms_payments = Helper::validate_request_context( 'sureforms_payments', 'page' );
1777 - $is_screen_sureforms_entries = Helper::validate_request_context( SRFM_ENTRIES, 'page' );
1778 - $is_screen_sureforms_learn = Helper::validate_request_context( 'sureforms_learn', 'page' );
1779 - $is_screen_quiz_empty_state = Helper::validate_request_context( 'sureforms_quiz_entries', 'page' );
1780 - $is_screen_survey_empty_state = Helper::validate_request_context( 'sureforms_survey_reports', 'page' );
1781 - $is_screen_partial_entries_empty_state = Helper::validate_request_context( 'sureforms_partial_entries', 'page' );
1782 - $is_post_type_sureforms_form = SRFM_FORMS_POST_TYPE === $current_screen->post_type;
455 + $is_screen_sureforms_menu = Helper::validate_request_context( 'sureforms_menu', 'page' );
456 + $is_screen_add_new_form = Helper::validate_request_context( 'add-new-form', 'page' );
457 + $is_screen_sureforms_form_settings = Helper::validate_request_context( 'sureforms_form_settings', 'page' );
458 + $is_screen_sureforms_entries = Helper::validate_request_context( SRFM_ENTRIES, 'page' );
459 + $is_post_type_sureforms_form = SRFM_FORMS_POST_TYPE === $current_screen->post_type;
1783 460
1784 - /**
1785 - * Check if the current screen is the SureForms Menu and AI Auth Email is present then we will add user type as registered.
1786 - * Compatibility with existing UI code that checks for this condition.
1787 - */
1788 - if ( $is_screen_sureforms_menu ) {
1789 - // If email is stored send the user type as registered else non-registered.
1790 - $localization_data['srfm_ai_details'] = [
1791 - 'type' => ! empty( get_option( 'srfm_ai_auth_user_email' ) ) ? 'registered' : 'non-registered',
1792 - ];
1793 - }
1794 -
1795 - // Add the Quizzes and Survey Reports nav items when pro is not active.
1796 - if ( ! Helper::has_pro() ) {
1797 - $localization_data['additional_header_nav_items'][] = [
1798 - 'slug' => 'sureforms_quiz_entries',
1799 - 'text' => __( 'Quizzes', 'sureforms' ),
1800 - 'link' => admin_url( 'admin.php?page=sureforms_quiz_entries' ),
1801 - ];
1802 - $localization_data['additional_header_nav_items'][] = [
1803 - 'slug' => 'sureforms_survey_reports',
1804 - 'text' => __( 'Survey Reports', 'sureforms' ),
1805 - 'link' => admin_url( 'admin.php?page=sureforms_survey_reports' ),
1806 - ];
1807 - $localization_data['additional_header_nav_items'][] = [
1808 - 'slug' => 'sureforms_partial_entries',
1809 - 'text' => __( 'Partial Entries', 'sureforms' ),
1810 - 'link' => admin_url( 'admin.php?page=sureforms_partial_entries' ),
1811 - ];
1812 - }
1813 -
1814 - $is_sureforms_screen = $is_screen_sureforms_menu || $is_post_type_sureforms_form || $is_screen_add_new_form || $is_screen_sureforms_forms || $is_screen_sureforms_form_settings || $is_screen_sureforms_entries || $is_screen_sureforms_payments || $is_screen_sureforms_learn || $is_screen_quiz_empty_state || $is_screen_survey_empty_state || $is_screen_partial_entries_empty_state;
1815 -
1816 - /**
1817 - * Filter to allow extending the SureForms dashboard screen check.
1818 - *
1819 - * @since 2.6.0
1820 - *
1821 - * @param bool $is_sureforms_screen Whether the current screen is a SureForms dashboard screen.
1822 - */
1823 - $is_sureforms_screen = apply_filters( 'srfm_is_dashboard_screen', $is_sureforms_screen );
1824 -
1825 - if ( $is_sureforms_screen ) {
461 + if ( $is_screen_sureforms_menu || $is_post_type_sureforms_form || $is_screen_add_new_form || $is_screen_sureforms_form_settings || $is_screen_sureforms_entries ) {
1826 462 $asset_handle = '-dashboard';
1827 463
1828 464 wp_enqueue_style( SRFM_SLUG . $asset_handle . '-font', 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap', [], SRFM_VER );
1829 465
@@ -1851,9 +487,9 @@
1851 487 update_option( 'srfm_pro_license_status', $current_license_status );
1852 488 }
1853 489 }
1854 490
1855 - $localization_data['security_settings_url'] = admin_url( '/admin.php?page=sureforms_form_settings&tab=security-settings&subpage=recaptcha' );
491 + $localization_data['security_settings_url'] = admin_url( '/admin.php?page=sureforms_form_settings&tab=security-settings' );
1856 492 $localization_data['integration_settings_url'] = admin_url( '/admin.php?page=sureforms_form_settings&tab=integration-settings' );
1857 493 wp_localize_script(
1858 494 SRFM_SLUG . $asset_handle,
1859 495 SRFM_SLUG . '_admin',
@@ -1862,12 +498,27 @@
1862 498 $localization_data
1863 499 )
1864 500 );
1865 501 wp_enqueue_style( SRFM_SLUG . '-dashboard', SRFM_URL . 'assets/build/dashboard.css', [], SRFM_VER, 'all' );
502 +
1866 503 }
1867 504
1868 - if ( $is_screen_sureforms_form_settings || $is_screen_sureforms_forms ) {
505 + if ( $is_screen_sureforms_form_settings ) {
1869 506 wp_enqueue_style( SRFM_SLUG . '-settings', $css_uri . 'backend/settings' . $file_prefix . $rtl . '.css', [], SRFM_VER );
507 +
508 + // if version is equal to or lower than 6.6.2 then add compatibility css.
509 + if ( version_compare( $wp_version, '6.6.2', '<=' ) ) {
510 + $srfm_inline_css = '
511 + .srfm-settings-page-container
512 + .components-toggle-control {
513 + .components-base-control__help{
514 + margin-left: 4em;
515 + }
516 + }
517 + }
518 + ';
519 + wp_add_inline_style( SRFM_SLUG . '-settings', $srfm_inline_css );
520 + }
1870 521 }
1871 522
1872 523 // Enqueue styles for the entries page.
1873 524 if ( $is_screen_sureforms_entries ) {
@@ -1873,158 +524,30 @@
1873 524 if ( $is_screen_sureforms_entries ) {
1874 525 $asset_handle = '-entries';
1875 526 wp_enqueue_script( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/entries.js', $script_info['dependencies'], SRFM_VER, true );
1876 527
1877 - wp_localize_script(
1878 - SRFM_SLUG . $asset_handle,
1879 - SRFM_SLUG . '_admin',
1880 - apply_filters(
1881 - SRFM_SLUG . '_admin_filter',
1882 - $localization_data
1883 - )
1884 - );
1885 528 $script_translations_handlers[] = SRFM_SLUG . $asset_handle;
1886 529 }
1887 530
1888 - // Enqueue scripts for the learn page.
1889 - if ( $is_screen_sureforms_learn ) {
1890 - $asset_handle = '-learn';
1891 - wp_enqueue_script( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/learn.js', $script_info['dependencies'], SRFM_VER, true );
531 + // Admin Submenu Styles.
532 + wp_enqueue_style( SRFM_SLUG . '-admin', $css_uri . 'backend/admin' . $file_prefix . $rtl . '.css', [], SRFM_VER );
1892 533
1893 - wp_localize_script(
1894 - SRFM_SLUG . $asset_handle,
1895 - SRFM_SLUG . '_admin',
1896 - apply_filters(
1897 - SRFM_SLUG . '_admin_filter',
1898 - $localization_data
1899 - )
1900 - );
1901 - $script_translations_handlers[] = SRFM_SLUG . $asset_handle;
1902 - }
534 + if ( 'edit-' . SRFM_FORMS_POST_TYPE === $current_screen->id ) {
535 + $asset_handle = 'page_header';
1903 536
1904 - // Enqueue scripts for the forms page.
1905 - if ( $is_screen_sureforms_forms ) {
1906 - $asset_handle = '-forms';
1907 -
1908 - $script_asset_path = SRFM_DIR . 'assets/build/forms.asset.php';
1909 - $script_info = file_exists( $script_asset_path )
1910 - ? include $script_asset_path
1911 - : [
1912 - 'dependencies' => [],
1913 - 'version' => SRFM_VER,
1914 - ];
1915 -
1916 - wp_enqueue_script( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/forms.js', $script_info['dependencies'], SRFM_VER, true );
1917 - wp_localize_script(
1918 - SRFM_SLUG . $asset_handle,
1919 - SRFM_SLUG . '_admin',
1920 - apply_filters(
1921 - SRFM_SLUG . '_admin_filter',
1922 - $localization_data
1923 - )
1924 - );
1925 - wp_enqueue_style( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/forms.css', [], SRFM_VER, 'all' );
1926 -
1927 - $script_translations_handlers[] = SRFM_SLUG . $asset_handle;
1928 - }
1929 -
1930 - // Enqueue scripts for the SureMail promotional page.
1931 - $is_screen_sureforms_smtp = Helper::validate_request_context( 'sureforms_smtp', 'page' );
1932 - if ( $is_screen_sureforms_smtp ) {
1933 - $asset_handle = 'suremail';
1934 -
1935 537 $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php';
1936 538 $script_info = file_exists( $script_asset_path )
1937 - ? include $script_asset_path
1938 - : [
1939 - 'dependencies' => [],
1940 - 'version' => SRFM_VER,
1941 - ];
1942 -
1943 - wp_enqueue_script( SRFM_SLUG . '-suremail', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true );
1944 - wp_enqueue_style( SRFM_SLUG . '-suremail', SRFM_URL . 'assets/build/suremail.css', [], SRFM_VER, 'all' );
1945 -
1946 - // Localize script for SureMail page.
1947 - $suremail_localization_data = [
1948 - 'ajax_url' => admin_url( 'admin-ajax.php' ),
1949 - 'admin_url' => admin_url(),
1950 - 'suremail_url' => 'https://sureforms.com/suremail/',
1951 - 'plugin_installer_nonce' => wp_create_nonce( 'updates' ),
1952 - 'sfPluginManagerNonce' => wp_create_nonce( 'sf_plugin_manager_nonce' ),
1953 - 'suremail_status' => file_exists( WP_PLUGIN_DIR . '/suremails/suremails.php' )
1954 - ? ( is_plugin_active( 'suremails/suremails.php' ) ? 'active' : 'installed' )
1955 - : 'not_installed',
539 + ? include $script_asset_path
540 + : [
541 + 'dependencies' => [],
542 + 'version' => SRFM_VER,
1956 543 ];
544 + wp_enqueue_script( SRFM_SLUG . '-form-page-header', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true );
545 + wp_enqueue_style( SRFM_SLUG . '-form-archive-styles', $css_uri . 'form-archive-styles' . $file_prefix . $rtl . '.css', [], SRFM_VER );
1957 546
1958 - wp_localize_script(
1959 - SRFM_SLUG . '-suremail',
1960 - SRFM_SLUG . '_admin',
1961 - apply_filters(
1962 - SRFM_SLUG . '_suremail_admin_filter',
1963 - $suremail_localization_data
1964 - )
1965 - );
1966 -
1967 - $script_translations_handlers[] = SRFM_SLUG . '-suremail';
547 + $script_translations_handlers[] = SRFM_SLUG . '-form-page-header';
1968 548 }
1969 549
1970 - // Enqueue scripts for the Quiz empty state page (free users only).
1971 - if ( $is_screen_quiz_empty_state && ! Helper::has_pro() ) {
1972 - $asset_handle = 'quizEmptyState';
1973 -
1974 - $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php';
1975 - $script_info = file_exists( $script_asset_path )
1976 - ? include $script_asset_path
1977 - : [
1978 - 'dependencies' => [],
1979 - 'version' => SRFM_VER,
1980 - ];
1981 -
1982 - wp_enqueue_script( SRFM_SLUG . '-quiz-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true );
1983 - wp_enqueue_style( SRFM_SLUG . '-quiz-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.css', [], SRFM_VER, 'all' );
1984 -
1985 - $script_translations_handlers[] = SRFM_SLUG . '-quiz-empty-state';
1986 - }
1987 -
1988 - // Enqueue scripts for the Survey Reports empty state page (free users only).
1989 - if ( $is_screen_survey_empty_state && ! Helper::has_pro() ) {
1990 - $asset_handle = 'surveyEmptyState';
1991 -
1992 - $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php';
1993 - $script_info = file_exists( $script_asset_path )
1994 - ? include $script_asset_path
1995 - : [
1996 - 'dependencies' => [],
1997 - 'version' => SRFM_VER,
1998 - ];
1999 -
2000 - wp_enqueue_script( SRFM_SLUG . '-survey-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true );
2001 - wp_enqueue_style( SRFM_SLUG . '-survey-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.css', [], SRFM_VER, 'all' );
2002 -
2003 - $script_translations_handlers[] = SRFM_SLUG . '-survey-empty-state';
2004 - }
2005 -
2006 - // Enqueue scripts for the Partial Entries empty state page (free users only).
2007 - if ( $is_screen_partial_entries_empty_state && ! Helper::has_pro() ) {
2008 - $asset_handle = 'partialEntriesEmptyState';
2009 -
2010 - $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php';
2011 - $script_info = file_exists( $script_asset_path )
2012 - ? include $script_asset_path
2013 - : [
2014 - 'dependencies' => [],
2015 - 'version' => SRFM_VER,
2016 - ];
2017 -
2018 - wp_enqueue_script( SRFM_SLUG . '-partial-entries-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true );
2019 - wp_enqueue_style( SRFM_SLUG . '-partial-entries-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.css', [], SRFM_VER, 'all' );
2020 -
2021 - $script_translations_handlers[] = SRFM_SLUG . '-partial-entries-empty-state';
2022 - }
2023 -
2024 - // Admin Submenu Styles.
2025 - wp_enqueue_style( SRFM_SLUG . '-admin', $css_uri . 'backend/admin' . $file_prefix . $rtl . '.css', [], SRFM_VER );
2026 -
2027 550 if ( $is_screen_sureforms_form_settings ) {
2028 551 $asset_handle = 'settings';
2029 552
2030 553 $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php';
@@ -2035,23 +558,45 @@
2035 558 'version' => SRFM_VER,
2036 559 ];
2037 560
2038 561 wp_enqueue_script( SRFM_SLUG . '-settings', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true );
562 + wp_enqueue_style( SRFM_SLUG . '-setting-styles', SRFM_URL . 'assets/build/' . $asset_handle . '.css', [ 'wp-components' ], SRFM_VER, 'all' );
2039 563 wp_localize_script(
2040 564 SRFM_SLUG . '-settings',
2041 565 SRFM_SLUG . '_admin',
2042 - apply_filters(
2043 - SRFM_SLUG . '_admin_filter',
2044 - $localization_data
2045 - )
566 + $localization_data
2046 567 );
2047 568
2048 - // Enqueue Tailwind and Quill editor styles for the settings page.
2049 - wp_enqueue_style( SRFM_SLUG . '-settings-build', SRFM_URL . 'assets/build/settings.css', [], SRFM_VER, 'all' );
2050 - wp_enqueue_style( SRFM_SLUG . '-reactQuill', SRFM_URL . 'assets/css/minified/deps/quill/quill.snow.css', [], SRFM_VER );
2051 - wp_add_inline_style( SRFM_SLUG . '-reactQuill', self::QUILL_1X_INLINE_CSS );
569 + $script_translations_handlers[] = SRFM_SLUG . '-settings';
570 + }
571 + if ( 'edit-' . SRFM_FORMS_POST_TYPE === $current_screen->id ) {
572 + wp_enqueue_script( SRFM_SLUG . '-form-archive', $js_uri . 'form-archive' . $file_prefix . '.js', [], SRFM_VER, true );
573 + wp_enqueue_script( SRFM_SLUG . '-export', $js_uri . 'export' . $file_prefix . '.js', [ 'wp-i18n' ], SRFM_VER, true );
574 + wp_localize_script(
575 + SRFM_SLUG . '-export',
576 + SRFM_SLUG . '_export',
577 + [
578 + 'ajaxurl' => admin_url( 'admin-ajax.php' ),
579 + 'srfm_export_nonce' => wp_create_nonce( 'export_form_nonce' ),
580 + 'site_url' => get_site_url(),
581 + 'srfm_import_endpoint' => '/wp-json/sureforms/v1/sureforms_import',
582 + 'import_form_nonce' => current_user_can( 'edit_posts' ) ? wp_create_nonce( 'wp_rest' ) : '',
583 + 'import_btn_string' => __( 'Import Form', 'sureforms' ),
584 + ]
585 + );
2052 586
2053 - $script_translations_handlers[] = SRFM_SLUG . '-settings';
587 + wp_enqueue_script( SRFM_SLUG . '-backend', $js_uri . 'backend' . $file_prefix . '.js', [], SRFM_VER, true );
588 + wp_localize_script(
589 + SRFM_SLUG . '-backend',
590 + SRFM_SLUG . '_backend',
591 + [
592 + 'site_url' => get_site_url(),
593 + ]
594 + );
595 +
596 + $script_translations_handlers[] = SRFM_SLUG . '-form-archive';
597 + $script_translations_handlers[] = SRFM_SLUG . '-export';
598 + $script_translations_handlers[] = SRFM_SLUG . '-backend';
2054 599 }
2055 600
2056 601 if ( $is_screen_add_new_form ) {
2057 602 wp_enqueue_style( SRFM_SLUG . '-template-picker', $css_uri . 'template-picker' . $file_prefix . $rtl . '.css', [], SRFM_VER );
@@ -2072,18 +617,18 @@
2072 617 SRFM_SLUG . '_admin',
2073 618 [
2074 619 'site_url' => get_site_url(),
2075 620 'plugin_url' => SRFM_URL,
621 + 'preview_images_url' => SRFM_URL . 'images/template-previews/',
2076 622 'admin_url' => admin_url( 'admin.php' ),
2077 623 'new_template_picker_base_url' => admin_url( 'post-new.php?post_type=sureforms_form' ),
2078 - 'capability' => Helper::current_user_can(),
2079 - 'template_picker_nonce' => Helper::current_user_can() ? wp_create_nonce( 'wp_rest' ) : '',
2080 - 'is_pro_active' => Helper::has_pro(),
624 + 'capability' => current_user_can( 'edit_posts' ),
625 + 'template_picker_nonce' => current_user_can( 'edit_posts' ) ? wp_create_nonce( 'wp_rest' ) : '',
626 + 'is_pro_active' => defined( 'SRFM_PRO_VER' ),
2081 627 'srfm_ai_usage_details' => AI_Helper::get_current_usage_details(),
2082 628 'is_pro_license_active' => AI_Helper::is_pro_license_active(),
2083 629 'srfm_ai_auth_user_email' => get_option( 'srfm_ai_auth_user_email' ),
2084 630 'pricing_page_url' => Helper::get_sureforms_website_url( 'pricing' ),
2085 - 'licensing_nonce' => wp_create_nonce( 'srfm_pro_licensing_nonce' ),
2086 631 ]
2087 632 );
2088 633
2089 634 $script_translations_handlers[] = SRFM_SLUG . '-template-picker';
@@ -2098,9 +643,8 @@
2098 643 'srfm/checkbox',
2099 644 'srfm/number',
2100 645 'srfm/inline-button',
2101 646 'srfm/advanced-heading',
2102 - 'srfm/payment',
2103 647 ]
2104 648 );
2105 649 if ( ! is_array( $default_allowed_quick_sidebar_blocks ) ) {
2106 650 $default_allowed_quick_sidebar_blocks = [];
@@ -2112,25 +656,22 @@
2112 656 }
2113 657 $quick_sidebar_allowed_blocks = get_option( 'srfm_quick_sidebar_allowed_blocks' );
2114 658 $quick_sidebar_allowed_blocks = ! empty( $quick_sidebar_allowed_blocks ) && is_array( $quick_sidebar_allowed_blocks ) ? $quick_sidebar_allowed_blocks : $default_allowed_quick_sidebar_blocks;
2115 659 $srfm_ajax_nonce = wp_create_nonce( 'srfm_ajax_nonce' );
660 + wp_enqueue_script( SRFM_SLUG . '-quick-action-siderbar', SRFM_URL . 'assets/build/quickActionSidebar.js', [], SRFM_VER, true );
661 + wp_localize_script(
662 + SRFM_SLUG . '-quick-action-siderbar',
663 + SRFM_SLUG . '_quick_sidebar_blocks',
664 + [
665 + 'allowed_blocks' => $quick_sidebar_allowed_blocks,
666 + 'srfm_enable_quick_action_sidebar' => $srfm_enable_quick_action_sidebar,
667 + 'srfm_ajax_nonce' => $srfm_ajax_nonce,
668 + 'srfm_ajax_url' => admin_url( 'admin-ajax.php' ),
669 + ]
670 + );
2116 671
2117 - if ( Helper::is_sureforms_admin_page() ) {
2118 - wp_enqueue_script( SRFM_SLUG . '-quick-action-siderbar', SRFM_URL . 'assets/build/quickActionSidebar.js', [], SRFM_VER, true );
2119 - wp_localize_script(
2120 - SRFM_SLUG . '-quick-action-siderbar',
2121 - SRFM_SLUG . '_quick_sidebar_blocks',
2122 - [
2123 - 'allowed_blocks' => $quick_sidebar_allowed_blocks,
2124 - 'srfm_enable_quick_action_sidebar' => $srfm_enable_quick_action_sidebar,
2125 - 'srfm_ajax_nonce' => $srfm_ajax_nonce,
2126 - 'srfm_ajax_url' => admin_url( 'admin-ajax.php' ),
2127 - ]
2128 - );
672 + $script_translations_handlers[] = SRFM_SLUG . '-quick-action-siderbar';
2129 673
2130 - $script_translations_handlers[] = SRFM_SLUG . '-quick-action-siderbar';
2131 - }
2132 -
2133 674 /**
2134 675 * Enqueuing SureTriggers Integration script.
2135 676 * This script loads suretriggers iframe in Intergations tab.
2136 677 */
@@ -2202,53 +743,40 @@
2202 743
2203 744 return $status;
2204 745 }
2205 746
747 + // Entries methods.
748 +
2206 749 /**
2207 - * Register Pro compatibility notices early for React pages.
750 + * Handle entry actions.
2208 751 *
2209 - * This method runs on admin_init (priority 5) to ensure notices are
2210 - * registered BEFORE admin_enqueue_scripts, so they're available when
2211 - * wp_localize_script runs.
2212 - *
2213 - * Hooked - admin_init (priority 5)
2214 - *
752 + * @since 0.0.13
2215 753 * @return void
2216 - * @since 2.5.0
2217 754 */
2218 - public function register_pro_compatibility_notices() {
2219 - // Early exit if Pro is not active, user lacks permissions, or Notice_Manager is unavailable.
2220 - if ( ! Helper::has_pro() || ! Helper::current_user_can() || ! class_exists( 'SRFM\Admin\Notice_Manager' ) ) {
755 + public function handle_entry_actions() {
756 + Entries_List_Table::process_bulk_actions();
757 +
758 + if ( ! isset( $_GET['page'] ) || SRFM_ENTRIES !== $_GET['page'] ) {
2221 759 return;
2222 760 }
2223 -
2224 - // Register version outdated notice for React pages.
2225 - if ( ! version_compare( SRFM_PRO_VER, SRFM_PRO_RECOMMENDED_VER, '>=' ) ) {
2226 - $pro_plugin_name = defined( 'SRFM_PRO_PRODUCT' ) ? SRFM_PRO_PRODUCT : 'SureForms Pro';
2227 - $react_outdated_message = sprintf(
2228 - // translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version.
2229 - esc_html__( 'SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version.', 'sureforms' ),
2230 - esc_html( SRFM_VER ),
2231 - esc_html( $pro_plugin_name ),
2232 - esc_html( SRFM_PRO_RECOMMENDED_VER )
2233 - );
2234 -
2235 - \SRFM\Admin\Notice_Manager::register_notice(
2236 - [
2237 - 'id' => 'sureforms-pro-version-outdated',
2238 - 'variant' => 'warning',
2239 - 'message' => $react_outdated_message,
2240 - 'actions' => [
2241 - [
2242 - 'label' => esc_html__( 'Update Now', 'sureforms' ),
2243 - 'url' => admin_url( 'update-core.php' ),
2244 - 'variant' => 'primary',
2245 - ],
2246 - ],
2247 - 'pages' => [ 'all' ],
2248 - ]
2249 - );
761 + if ( ! isset( $_GET['entry_id'] ) || ! isset( $_GET['action'] ) ) {
762 + return;
2250 763 }
764 + if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'srfm_entries_action' ) ) {
765 + wp_die( esc_html__( 'Nonce verification failed.', 'sureforms' ) );
766 + }
767 + $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '';
768 + $entry_id = Helper::get_integer_value( sanitize_text_field( wp_unslash( $_GET['entry_id'] ) ) );
769 + $view = isset( $_GET['view'] ) ? sanitize_text_field( wp_unslash( $_GET['view'] ) ) : '';
770 + if ( $entry_id > 0 ) {
771 + if ( 'read' === $action && 'details' === $view ) {
772 + $entry_status = Entries::get( $entry_id )['status'];
773 + if ( 'trash' === $entry_status ) {
774 + wp_die( esc_html__( 'You cannot view this entry because it is in trash.', 'sureforms' ) );
775 + }
776 + }
777 + Entries_List_Table::handle_entry_status( $entry_id, $action, $view );
778 + }
2251 779 }
2252 780
2253 781 /**
2254 782 * Admin Notice Callback if sureforms pro is out of date.
@@ -2258,9 +786,10 @@
2258 786 * @return void
2259 787 * @since 1.0.4
2260 788 */
2261 789 public function srfm_pro_version_compatibility() {
2262 - if ( ! Helper::has_pro() ) {
790 + $plugin_file = 'sureforms-pro/sureforms-pro.php';
791 + if ( ! is_plugin_active( $plugin_file ) || ! defined( 'SRFM_PRO_VER' ) ) {
2263 792 return;
2264 793 }
2265 794
2266 795 if ( empty( get_current_screen() ) ) {
@@ -2266,9 +795,9 @@
2266 795 if ( empty( get_current_screen() ) ) {
2267 796 return;
2268 797 }
2269 798
2270 - if ( ! Helper::current_user_can() ) {
799 + if ( ! current_user_can( 'update_plugins' ) ) {
2271 800 return;
2272 801 }
2273 802
2274 803 $srfm_pro_license_status = get_option( 'srfm_pro_license_status', '' );
@@ -2284,221 +813,36 @@
2284 813 $pro_plugin_name = defined( 'SRFM_PRO_PRODUCT' ) ? SRFM_PRO_PRODUCT : 'SureForms Pro';
2285 814 $message = '';
2286 815 $url = admin_url( 'admin.php?page=sureforms_form_settings&tab=account-settings' );
2287 816 if ( 'unlicensed' === $srfm_pro_license_status ) {
2288 - ob_start();
2289 - ?>
2290 - <p>
2291 - <?php
2292 - printf(
2293 - // translators: %1$s: Opening anchor tag with URL, %2$s: Closing anchor tag, %3$s: SureForms Pro Plugin Name.
2294 - esc_html__( 'Please %1$sactivate%2$s your copy of %3$s to get new features, access support, receive update notifications, and more.', 'sureforms' ),
2295 - '<a href="' . esc_url( $url ) . '">',
2296 - '</a>',
2297 - '<i>' . esc_html( $pro_plugin_name ) . '</i>'
2298 - );
2299 - ?>
2300 - </p>
2301 - <?php
2302 - $message = ob_get_clean();
817 + $message = '<p>' . sprintf(
818 + // translators: %1$s: Opening anchor tag with URL, %2$s: Closing anchor tag, %3$s: SureForms Pro Plugin Name.
819 + esc_html__( 'Please %1$sactivate%2$s your copy of %3$s to get new features, access support, receive update notifications, and more.', 'sureforms' ),
820 + '<a href="' . esc_url( $url ) . '">',
821 + '</a>',
822 + '<i>' . esc_html( $pro_plugin_name ) . '</i>'
823 + ) . '</p>';
2303 824 }
2304 825
2305 826 if ( ! version_compare( SRFM_PRO_VER, SRFM_PRO_RECOMMENDED_VER, '>=' ) ) {
2306 - ob_start();
2307 - ?>
2308 - <p>
2309 - <?php
2310 - printf(
2311 - // translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version, %4$s: Anchor tag open, %5$s: Closing anchor tag.
2312 - esc_html__( 'SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version from %4$shere%5$s.', 'sureforms' ),
2313 - esc_html( SRFM_VER ),
2314 - esc_html( $pro_plugin_name ),
2315 - esc_html( SRFM_PRO_RECOMMENDED_VER ),
2316 - '<a href="' . esc_url( admin_url( 'update-core.php' ) ) . '">',
2317 - '</a>'
2318 - );
2319 - ?>
2320 - </p>
2321 - <?php
2322 - $message .= ob_get_clean();
827 + $message .= '<p>' . sprintf(
828 + // translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version, %4$s: Anchor tag open, %5$s: Closing anchor tag.
829 + esc_html__( 'SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version from %4$shere%5$s.', 'sureforms' ),
830 + esc_html( SRFM_VER ),
831 + esc_html( $pro_plugin_name ),
832 + esc_html( SRFM_PRO_RECOMMENDED_VER ),
833 + '<a href=' . esc_url( admin_url( 'update-core.php' ) ) . '>',
834 + '</a>'
835 + ) . '</p>';
2323 836 }
2324 837
2325 838 if ( ! empty( $message ) ) {
2326 839 // Phpcs ignore comment is required as $message variable is already escaped.
2327 - ?>
2328 - <div class="notice notice-warning"><?php echo wp_kses_post( $message ); ?></div>
2329 - <?php
840 + echo '<div class="notice notice-warning">' . wp_kses_post( $message ) . '</div>';
2330 841 }
2331 842 }
2332 843
2333 844 /**
2334 - * Display a notice to the user about providing a review.
2335 - *
2336 - * @since 2.5.2
2337 - * @return void
2338 - */
2339 - public function display_srfm_rating_notice() {
2340 - // Only show to admins.
2341 - if ( ! Helper::current_user_can() ) {
2342 - return;
2343 - }
2344 -
2345 - // Allow the notice to be disabled.
2346 - if ( ! apply_filters( 'srfm_show_rating_notice', true ) ) {
2347 - return;
2348 - }
2349 -
2350 - Astra_Notices::add_notice(
2351 - [
2352 - 'id' => 'srfm-plugin-review-notice',
2353 - 'type' => '',
2354 - 'message' => $this->build_notice_markup(
2355 - esc_html__( 'Amazing! SureForms is powering your forms and submissions - let\'s keep growing together!', 'sureforms' ),
2356 - esc_html__( 'If SureForms has been helpful, would you mind taking a moment to leave a 5-star review on WordPress.org?', 'sureforms' ),
2357 - esc_url( 'https://wordpress.org/support/plugin/sureforms/reviews/' ),
2358 - esc_html__( 'Rate SureForms', 'sureforms' ),
2359 - esc_html__( 'Maybe later', 'sureforms' ),
2360 - esc_html__( 'I already did', 'sureforms' ),
2361 - WEEK_IN_SECONDS,
2362 - true
2363 - ),
2364 - 'repeat-notice-after' => WEEK_IN_SECONDS,
2365 - 'show_if' => $this->maybe_display_rating_notice(),
2366 - 'display-with-other-notices' => true,
2367 - ]
2368 - );
2369 -
2370 - add_action( 'astra_notice_after_markup_srfm-plugin-review-notice', [ $this, 'enqueue_notice_response_script' ] );
2371 - }
2372 -
2373 - /**
2374 - * Display a "Getting Started" admin notice for new users who haven't yet
2375 - * reached the rating-notice milestone (3+ forms or 3+ entries).
2376 - *
2377 - * The Astra Notices library handles the 7-day delay via the
2378 - * `display-notice-after` parameter.
2379 - *
2380 - * @since 2.5.2
2381 - * @return void
2382 - */
2383 - public function display_srfm_getting_started_notice() {
2384 - // Only show to admins.
2385 - if ( ! Helper::current_user_can() ) {
2386 - return;
2387 - }
2388 -
2389 - // Allow the notice to be disabled programmatically.
2390 - if ( ! apply_filters( 'srfm_show_getting_started_notice', true ) ) {
2391 - return;
2392 - }
2393 -
2394 - Astra_Notices::add_notice(
2395 - [
2396 - 'id' => 'srfm-getting-started-notice',
2397 - 'type' => '',
2398 - 'message' => $this->build_notice_markup(
2399 - esc_html__( 'SureForms is ready to power your forms — explore what\'s possible!', 'sureforms' ),
2400 - esc_html__( 'Manage your forms, track submissions, and discover features like AI Form Builder, payment integrations, and more from the SureForms dashboard.', 'sureforms' ),
2401 - esc_url( admin_url( 'admin.php?page=sureforms_menu' ) ),
2402 - esc_html__( 'Go to Dashboard', 'sureforms' ),
2403 - esc_html__( 'Maybe later', 'sureforms' ),
2404 - esc_html__( 'I already know', 'sureforms' ),
2405 - WEEK_IN_SECONDS
2406 - ),
2407 - 'repeat-notice-after' => WEEK_IN_SECONDS,
2408 - 'show_if' => ! $this->maybe_display_rating_notice(),
2409 - 'display-notice-after' => WEEK_IN_SECONDS,
2410 - 'display-with-other-notices' => true,
2411 - ]
2412 - );
2413 -
2414 - add_action( 'astra_notice_after_markup_srfm-getting-started-notice', [ $this, 'enqueue_notice_response_script' ] );
2415 - }
2416 -
2417 - /**
2418 - * Enqueue the notice response analytics script.
2419 - *
2420 - * Called via the astra_notice_after_markup_{id} hook so the script
2421 - * only loads when a SureForms notice is actually rendered.
2422 - *
2423 - * @since 2.5.2
2424 - * @return void
2425 - */
2426 - public function enqueue_notice_response_script() {
2427 - if ( wp_script_is( 'srfm-notice-response', 'enqueued' ) ) {
2428 - return;
2429 - }
2430 -
2431 - wp_enqueue_script(
2432 - 'srfm-notice-response',
2433 - SRFM_URL . 'admin/assets/js/notice-response.js',
2434 - [],
2435 - SRFM_VER,
2436 - true
2437 - );
2438 -
2439 - wp_localize_script(
2440 - 'srfm-notice-response',
2441 - 'srfmNoticeResponse',
2442 - [
2443 - 'ajaxurl' => admin_url( 'admin-ajax.php' ),
2444 - 'nonce' => wp_create_nonce( 'srfm_notice_response' ),
2445 - ]
2446 - );
2447 - }
2448 -
2449 - /**
2450 - * Handle the notice response AJAX request.
2451 - *
2452 - * Validates the request and records the analytics event
2453 - * for the notice button that was clicked.
2454 - *
2455 - * @since 2.5.2
2456 - * @return void
2457 - */
2458 - public function handle_notice_response() {
2459 - if ( ! check_ajax_referer( 'srfm_notice_response', 'nonce', false ) ) {
2460 - wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 );
2461 - }
2462 -
2463 - if ( ! Helper::current_user_can() ) {
2464 - wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 );
2465 - }
2466 -
2467 - $notice_id = isset( $_POST['notice_id'] ) ? sanitize_text_field( wp_unslash( $_POST['notice_id'] ) ) : '';
2468 - $button = isset( $_POST['button'] ) ? sanitize_text_field( wp_unslash( $_POST['button'] ) ) : '';
2469 -
2470 - $valid = [
2471 - 'srfm-getting-started-notice' => [
2472 - 'go_to_dashboard' => 'getting_started_notice_cta',
2473 - 'maybe_later' => 'getting_started_notice_snooze',
2474 - 'dismissed' => 'getting_started_notice_dismiss',
2475 - ],
2476 - 'srfm-plugin-review-notice' => [
2477 - 'rate_sureforms' => 'rating_notice_cta',
2478 - 'maybe_later' => 'rating_notice_snooze',
2479 - 'dismissed' => 'rating_notice_dismiss',
2480 - ],
2481 - // The "Finish setting up" prompt (#3030): three CTAs, plus the ✕.
2482 - 'srfm-thankyou-prompt' => [
2483 - 'edit_form' => 'thankyou_notice_edit_form',
2484 - 'set_replies' => 'thankyou_notice_set_replies',
2485 - 'edit_thankyou' => 'thankyou_notice_edit_thankyou',
2486 - 'dismissed' => 'thankyou_notice_dismiss',
2487 - ],
2488 - ];
2489 -
2490 - if ( ! isset( $valid[ $notice_id ][ $button ] ) ) {
2491 - wp_send_json_error( [ 'message' => __( 'Invalid parameters.', 'sureforms' ) ], 400 );
2492 - }
2493 -
2494 - $event_name = $valid[ $notice_id ][ $button ];
2495 - Analytics::events()->track( $event_name, $button );
2496 -
2497 - wp_send_json_success();
2498 - }
2499 -
2500 - /**
2501 845 * Disables the capabilities for WPForms to avoid conflicts when enqueueing
2502 846 * scripts and styles for WPForms.
2503 847 *
2504 848 * This function is intended to prevent any potential conflicts that may arise
@@ -2519,858 +863,5 @@
2519 863 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2520 864 $post_type = $post_id ? get_post_type( $post_id ) : sanitize_text_field( wp_unslash( $_REQUEST['post_type'] ?? '' ) );
2521 865 return SRFM_FORMS_POST_TYPE === $post_type ? false : $user_can;
2522 866 }
2523 -
2524 - /**
2525 - * Enqueueus the admin pointer script and styles.
2526 - *
2527 - * @return void
2528 - * @since 1.8.0
2529 - */
2530 - public function enqueue_admin_pointer() {
2531 - if ( ! $this->is_admin_pointer_visible() ) {
2532 - return;
2533 - }
2534 - wp_enqueue_style( 'wp-pointer' );
2535 - wp_enqueue_script( 'wp-pointer' );
2536 - wp_enqueue_script(
2537 - 'sureforms-admin-pointer',
2538 - plugins_url( 'admin/assets/js/sureforms-pointer.js', SRFM_FILE ),
2539 - [ 'wp-pointer', 'jquery' ],
2540 - SRFM_VER,
2541 - true
2542 - );
2543 - wp_localize_script(
2544 - 'sureforms-admin-pointer',
2545 - 'sureformsPointerData',
2546 - [
2547 - 'ajaxurl' => admin_url( 'admin-ajax.php' ),
2548 - 'pointer_nonce' => wp_create_nonce( 'sureforms_pointer_action' ),
2549 - ]
2550 - );
2551 - }
2552 -
2553 - /**
2554 - * Ajax handler for pointer popup visibility.
2555 - *
2556 - * @return void
2557 - * @since 1.8.0
2558 - */
2559 - public function pointer_should_show() {
2560 - // Security: Check user capability.
2561 - if ( ! Helper::current_user_can() ) {
2562 - wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 );
2563 - }
2564 - // Security: Nonce check.
2565 - if ( empty( $_POST['pointer_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pointer_nonce'] ) ), 'sureforms_pointer_action' ) ) {
2566 - wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 );
2567 - }
2568 -
2569 - $content_markup = sprintf(
2570 - /* translators: 1: opening span, 2: opening strong (inline), 3: closing strong, 4: closing span, 5: opening strong (block), 6: closing strong */
2571 - __( '%1$sGet started by %2$sbuilding your first form%3$s.%4$s%5$sExperience the power of our intuitive AI Form Builder%6$s', 'sureforms' ),
2572 - '<span>',
2573 - '<strong>',
2574 - '</strong>',
2575 - '</span><br/>',
2576 - '<strong style="font-size:1.1em;">',
2577 - '</strong>'
2578 - );
2579 - wp_send_json(
2580 - [
2581 - 'show' => true,
2582 - 'title' => esc_html( __( 'SureForms is waiting for you!', 'sureforms' ) ),
2583 - 'content' => wp_kses_post( $content_markup ),
2584 - 'button_text' => esc_html( __( 'Build My First Form', 'sureforms' ) ),
2585 - 'dismiss' => esc_html( __( 'Dismiss', 'sureforms' ) ),
2586 - 'button_url' => admin_url( 'admin.php?page=add-new-form' ),
2587 - ]
2588 - );
2589 - }
2590 -
2591 - /**
2592 - * Ajax callback for pointer popup dismissed action.
2593 - *
2594 - * @return void
2595 - * @since 1.8.0
2596 - */
2597 - public function pointer_dismissed() {
2598 - // Security: Check user capability.
2599 - if ( ! Helper::current_user_can() ) {
2600 - wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 );
2601 - }
2602 - // Security: Nonce check.
2603 - if ( empty( $_POST['pointer_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pointer_nonce'] ) ), 'sureforms_pointer_action' ) ) {
2604 - wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 );
2605 - }
2606 - // Use Helper to update srfm_options key.
2607 - Helper::update_srfm_option( 'pointer_popup_dismissed', time() );
2608 -
2609 - wp_send_json_success();
2610 - }
2611 -
2612 - /**
2613 - * Ajax pointer accepted CTA callback.
2614 - *
2615 - * @return void
2616 - * @since 1.8.0
2617 - */
2618 - public function pointer_accepted_cta() {
2619 - // Security: Check user capability.
2620 - if ( ! Helper::current_user_can() ) {
2621 - wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 );
2622 - }
2623 - // Security: Nonce check.
2624 - if ( empty( $_POST['pointer_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pointer_nonce'] ) ), 'sureforms_pointer_action' ) ) {
2625 - wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 );
2626 - }
2627 - // Use Helper to update srfm_options key.
2628 - Helper::update_srfm_option( 'pointer_popup_accepted', time() );
2629 -
2630 - wp_send_json_success();
2631 - }
2632 -
2633 - /**
2634 - * Maybe register the dashboard widget based on entries.
2635 - *
2636 - * @return void
2637 - * @since 1.9.1
2638 - */
2639 - public function maybe_register_dashboard_widget() {
2640 -
2641 - // Only for users with manage_options capability.
2642 - if ( ! Helper::current_user_can() ) {
2643 - return;
2644 - }
2645 -
2646 - // Register the AI quick draft widget for capable users (the capability gate above applies); unlike the recent-entries widget below, it is not conditional on having entries.
2647 - add_action( 'wp_dashboard_setup', [ $this, 'register_ai_dashboard_widget' ] );
2648 -
2649 - // Quick check if there are any entries in the last 7 days.
2650 - $seven_days_ago = strtotime( '-7 days' );
2651 - $total_entries = Entries::get_entries_count_after( $seven_days_ago );
2652 -
2653 - // Only add the dashboard setup hook if there are entries.
2654 - if ( $total_entries > 0 ) {
2655 - // Get forms with entries (limit 4 for dashboard widget).
2656 - $this->dashboard_widget_data = Helper::get_forms_with_entry_counts( $seven_days_ago, 4 );
2657 -
2658 - // Only show dashboard widget if there are forms with entries.
2659 - if ( ! empty( $this->dashboard_widget_data ) ) {
2660 - add_action( 'wp_dashboard_setup', [ $this, 'register_dashboard_widget' ] );
2661 - }
2662 - }
2663 - }
2664 -
2665 - /**
2666 - * Register the dashboard widget.
2667 - *
2668 - * @return void
2669 - * @since 1.9.1
2670 - */
2671 - public function register_dashboard_widget() {
2672 - // Add the widget with high priority to position it at the top.
2673 - wp_add_dashboard_widget(
2674 - 'sureforms_recent_entries',
2675 - __( 'SureForms', 'sureforms' ),
2676 - [ $this, 'render_dashboard_widget' ],
2677 - null,
2678 - null,
2679 - 'normal',
2680 - 'high'
2681 - );
2682 - }
2683 -
2684 - /**
2685 - * Register the AI quick draft dashboard widget.
2686 - *
2687 - * @return void
2688 - * @since 2.12.1
2689 - */
2690 - public function register_ai_dashboard_widget() {
2691 - wp_add_dashboard_widget(
2692 - 'sureforms_ai_quick_draft',
2693 - __( 'SureForms AI Quick Draft', 'sureforms' ),
2694 - [ $this, 'render_ai_dashboard_widget' ],
2695 - null,
2696 - null,
2697 - 'normal',
2698 - 'high'
2699 - );
2700 - }
2701 -
2702 - /**
2703 - * Render AI quick draft dashboard widget content.
2704 - *
2705 - * @return void
2706 - * @since 2.12.1
2707 - */
2708 - public function render_ai_dashboard_widget() {
2709 - ?>
2710 - <div class="srfm-ai-dashboard-widget">
2711 - <p>
2712 - <?php esc_html_e( 'Describe the form and let SureForms AI generate it for you.', 'sureforms' ); ?>
2713 - </p>
2714 - <label for="srfm-ai-dashboard-prompt" class="screen-reader-text">
2715 - <?php esc_html_e( 'Describe your form', 'sureforms' ); ?>
2716 - </label>
2717 - <textarea
2718 - id="srfm-ai-dashboard-prompt"
2719 - class="widefat"
2720 - rows="5"
2721 - maxlength="2000"
2722 - placeholder="<?php esc_attr_e( 'Example: Create a contact form with name, email, phone, and message fields.', 'sureforms' ); ?>"
2723 - ></textarea>
2724 - <p style="margin-top:10px;margin-bottom:0;display:flex;align-items:center;gap:10px;">
2725 - <button type="button" class="button button-primary" id="srfm-ai-dashboard-generate" disabled>
2726 - <?php esc_html_e( 'Create New Form', 'sureforms' ); ?>
2727 - </button>
2728 - <span id="srfm-ai-dashboard-char-count" style="color:#646970;">0/2000</span>
2729 - </p>
2730 - </div>
2731 - <?php
2732 - }
2733 -
2734 - /**
2735 - * Enqueue the AI quick draft dashboard widget script on the dashboard screen.
2736 - *
2737 - * The widget's behavior lives here (attached via wp_add_inline_script) rather than as an
2738 - * inline <script> in the render callback, so it passes Plugin Check and keeps server values
2739 - * out of the markup. Server values are passed through wp_localize_script.
2740 - *
2741 - * @param string $hook_suffix The current admin page hook suffix.
2742 - * @return void
2743 - * @since 2.12.1
2744 - */
2745 - public function enqueue_ai_dashboard_widget_assets( $hook_suffix ) {
2746 - // Only on the main dashboard, and only for capable users (matches the widget gate).
2747 - if ( 'index.php' !== $hook_suffix || ! Helper::current_user_can() ) {
2748 - return;
2749 - }
2750 -
2751 - // Register an inline-only handle (empty src) — the WordPress-core pattern for attaching
2752 - // localized data plus an inline script without shipping a separate asset file.
2753 - wp_register_script( 'srfm-ai-dashboard-widget', '', [], SRFM_VER, true );
2754 - wp_enqueue_script( 'srfm-ai-dashboard-widget' );
2755 -
2756 - wp_localize_script(
2757 - 'srfm-ai-dashboard-widget',
2758 - 'srfmAiDashboardWidget',
2759 - [
2760 - 'redirectUrl' => admin_url( 'admin.php?page=add-new-form' ),
2761 - 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
2762 - 'nonce' => wp_create_nonce( 'srfm_ai_widget_usage' ),
2763 - 'redirectingTxt' => __( 'Redirecting...', 'sureforms' ),
2764 - ]
2765 - );
2766 -
2767 - $inline_script = <<<'JS'
2768 -( function () {
2769 - const config = window.srfmAiDashboardWidget || {};
2770 - const generateButton = document.getElementById( 'srfm-ai-dashboard-generate' );
2771 - const promptField = document.getElementById( 'srfm-ai-dashboard-prompt' );
2772 - const charCount = document.getElementById( 'srfm-ai-dashboard-char-count' );
2773 - if ( ! generateButton || ! promptField ) {
2774 - return;
2775 - }
2776 -
2777 - const updateWidgetState = function () {
2778 - const promptValue = promptField.value.trim();
2779 - generateButton.disabled = ! promptValue;
2780 - if ( charCount ) {
2781 - charCount.textContent = `${ promptField.value.length }/2000`;
2782 - }
2783 - };
2784 -
2785 - const triggerGeneration = function () {
2786 - const prompt = promptField.value.trim();
2787 - if ( ! prompt ) {
2788 - promptField.focus();
2789 - return;
2790 - }
2791 -
2792 - generateButton.disabled = true;
2793 - generateButton.textContent = config.redirectingTxt;
2794 -
2795 - const redirectUrl = new URL( config.redirectUrl, window.location.origin );
2796 - redirectUrl.searchParams.set( 'srfm_ai_dashboard_prompt', prompt );
2797 -
2798 - const requestBody = new URLSearchParams();
2799 - requestBody.append( 'action', 'srfm_ai_widget_usage' );
2800 - requestBody.append( 'nonce', config.nonce );
2801 -
2802 - fetch( config.ajaxUrl, {
2803 - method: 'POST',
2804 - credentials: 'same-origin',
2805 - headers: {
2806 - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
2807 - },
2808 - body: requestBody.toString(),
2809 - } ).finally( function () {
2810 - window.location.href = redirectUrl.toString();
2811 - } );
2812 - };
2813 -
2814 - promptField.addEventListener( 'input', updateWidgetState );
2815 - generateButton.addEventListener( 'click', triggerGeneration );
2816 - promptField.addEventListener( 'keydown', function ( event ) {
2817 - if ( event.key === 'Enter' && ( event.metaKey || event.ctrlKey ) ) {
2818 - event.preventDefault();
2819 - triggerGeneration();
2820 - }
2821 - } );
2822 -
2823 - updateWidgetState();
2824 -}() );
2825 -JS;
2826 -
2827 - wp_add_inline_script( 'srfm-ai-dashboard-widget', $inline_script );
2828 - }
2829 -
2830 - /**
2831 - * Track AI dashboard widget usage.
2832 - *
2833 - * @return void
2834 - * @since 2.12.1
2835 - */
2836 - public function track_ai_widget_usage() {
2837 - if ( ! check_ajax_referer( 'srfm_ai_widget_usage', 'nonce', false ) ) {
2838 - wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 );
2839 - }
2840 -
2841 - if ( ! Helper::current_user_can() ) {
2842 - wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 );
2843 - }
2844 -
2845 - $current_count = (int) Helper::get_srfm_option( 'ai_dashboard_widget_uses', 0 ) + 1;
2846 - Helper::update_srfm_option( 'ai_dashboard_widget_uses', $current_count );
2847 -
2848 - // Emit an analytics event so usage lands in the warehouse via events_record.
2849 - // $force = true because this is a cumulative counter, not a one-time event —
2850 - // it must re-send the latest count each cycle (bypasses one-time dedup).
2851 - Analytics::events()->track( 'ai_dashboard_widget_used', (string) $current_count, [], true );
2852 -
2853 - wp_send_json_success();
2854 - }
2855 -
2856 - /**
2857 - * Render the dashboard widget content.
2858 - *
2859 - * @return void
2860 - * @since 1.9.1
2861 - */
2862 - public function render_dashboard_widget() {
2863 - // Use the pre-fetched data to avoid duplicate queries.
2864 - $entries_data = $this->dashboard_widget_data;
2865 -
2866 - // Display the widget content.
2867 - ?>
2868 - <div class="srfm-dashboard-widget">
2869 - <div class="srfm-widget-header">
2870 - <h3 class="srfm-widget-title">
2871 - <?php esc_html_e( 'Recent Entries', 'sureforms' ); ?>
2872 - <span class="srfm-widget-subtitle"><?php esc_html_e( '( Last 7 days )', 'sureforms' ); ?></span>
2873 - </h3>
2874 - <a href="<?php echo esc_url( admin_url( 'admin.php?page=sureforms_entries' ) ); ?>" class="srfm-widget-view-link">
2875 - <?php esc_html_e( 'View', 'sureforms' ); ?>
2876 - </a>
2877 - </div>
2878 -
2879 - <div class="srfm-table-wrapper">
2880 - <table class="srfm-entries-table">
2881 - <thead>
2882 - <tr>
2883 - <th><?php esc_html_e( 'Form Name', 'sureforms' ); ?></th>
2884 - <th><?php esc_html_e( 'Entries', 'sureforms' ); ?></th>
2885 - </tr>
2886 - </thead>
2887 - <tbody>
2888 - <?php foreach ( $entries_data as $form_data ) { ?>
2889 - <tr>
2890 - <td class="form-name"><?php echo esc_html( $form_data['title'] ); ?></td>
2891 - <td class="entry-count"><?php echo esc_html( $form_data['count'] ); ?></td>
2892 - </tr>
2893 - <?php } ?>
2894 - </tbody>
2895 - </table>
2896 - </div>
2897 -
2898 - <?php
2899 - // Render footer if applicable.
2900 - $this->render_dashboard_widget_footer( $entries_data );
2901 - ?>
2902 - </div>
2903 - <?php
2904 - }
2905 -
2906 - /**
2907 - * Build the setup-card payload (uncached). See get_form_setup_card().
2908 - *
2909 - * @since 2.12.4
2910 - * @return array<string,mixed>|null Card payload, or null when there is no candidate.
2911 - */
2912 - private static function compute_form_setup_card() {
2913 - if ( ! defined( 'SRFM_FORMS_POST_TYPE' ) || ! post_type_exists( SRFM_FORMS_POST_TYPE ) ) {
2914 - return null;
2915 - }
2916 -
2917 - // Negative cache. Deliberately not a `defined( 'ASTRA_SITES_VER' )` check:
2918 - // Starter Templates defines that constant in its main plugin file, so it only
2919 - // exists while the plugin is active, yet neither its uninstall.php nor its
2920 - // deactivation hook removes the import marker. Gating on the constant would
2921 - // silently switch this feature off for the very people it targets — anyone who
2922 - // imported a starter template and then removed the one-shot import plugin.
2923 - if ( 'no' === get_transient( self::NO_IMPORTED_FORMS_TRANSIENT ) ) {
2924 - return null;
2925 - }
2926 -
2927 - // Only forms created from an Astra Sites starter template — those carry the
2928 - // marker Starter Templates stamps on imported posts (self::ASTRA_SITES_IMPORT_META).
2929 - // Prime post + meta caches (the loop reads title, permalink and edit link
2930 - // per candidate) so this is a single query, not a follow-up per form.
2931 - $query = new \WP_Query(
2932 - [
2933 - 'post_type' => SRFM_FORMS_POST_TYPE,
2934 - 'post_status' => [ 'publish', 'draft', 'pending' ],
2935 - 'posts_per_page' => 10,
2936 - // ID breaks the tie: a starter-template import creates several forms
2937 - // within the same second, so post_date alone leaves "the newest form"
2938 - // up to MySQL and it can differ between page loads.
2939 - 'orderby' => [
2940 - 'date' => 'DESC',
2941 - 'ID' => 'DESC',
2942 - ],
2943 - 'no_found_rows' => true,
2944 - 'update_post_meta_cache' => true,
2945 - 'update_post_term_cache' => false,
2946 - 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Bounded to 10 recent forms; dashboard-only.
2947 - [
2948 - 'key' => self::ASTRA_SITES_IMPORT_META,
2949 - 'compare' => 'EXISTS',
2950 - ],
2951 - ],
2952 - ]
2953 - );
2954 -
2955 - // Nothing on this site carries the marker — remember that, so the query does
2956 - // not repeat on every load. Keyed on the query result rather than on anything
2957 - // user-specific, so it is safe to share, and invalidated the moment a post is
2958 - // stamped (see invalidate_starter_template_cache()).
2959 - if ( empty( $query->posts ) ) {
2960 - set_transient( self::NO_IMPORTED_FORMS_TRANSIENT, 'no', WEEK_IN_SECONDS );
2961 - }
2962 -
2963 - foreach ( $query->posts as $post ) {
2964 - $form_id = (int) $post->ID;
2965 -
2966 - if ( ! current_user_can( 'edit_post', $form_id ) ) {
2967 - continue;
2968 - }
2969 -
2970 - $edit_link = get_edit_post_link( $form_id, 'raw' );
2971 -
2972 - if ( empty( $edit_link ) ) {
2973 - continue;
2974 - }
2975 -
2976 - // The steps are shown as optional next-steps — their completion is not
2977 - // computed, so the widget simply lists the actions the owner can take.
2978 - return [
2979 - 'id' => $form_id,
2980 - 'title' => get_the_title( $form_id ),
2981 - 'edit_url' => $edit_link,
2982 - // Deep-links to the email-notification panel where supported; falls
2983 - // back to opening the editor when the focus handler isn't present.
2984 - 'email_url' => add_query_arg( 'srfm_focus', 'notifications', $edit_link ),
2985 - // Deep-links to the Form Confirmation panel (the Thank You message).
2986 - 'thankyou_url' => add_query_arg( 'srfm_focus', 'thankyou', $edit_link ),
2987 - // Front-end instant-form page. get_permalink() only yields a working
2988 - // URL for published forms; a draft/pending form has no public URL, so
2989 - // omit the view link there (the empty() guard hides the icon).
2990 - 'view_url' => 'publish' === $post->post_status ? (string) get_permalink( $form_id ) : '',
2991 - ];
2992 - }
2993 -
2994 - return null;
2995 - }
2996 -
2997 - /**
2998 - * Build the Thank You prompt payload (uncached). See get_thankyou_prompt_forms().
2999 - *
3000 - * @since 2.12.4
3001 - * @return array<int,array<string,mixed>> One entry, or none.
3002 - */
3003 - private static function compute_thankyou_prompt_forms() {
3004 - if ( ! defined( 'SRFM_FORMS_POST_TYPE' ) || ! post_type_exists( SRFM_FORMS_POST_TYPE ) ) {
3005 - return [];
3006 - }
3007 -
3008 - // Negative cache — this notice renders on every admin screen, so keeping the
3009 - // query off installs that can never match is what matters here. See
3010 - // self::NO_IMPORTED_FORMS_TRANSIENT for why this is not gated on whether
3011 - // Starter Templates is still active: the marker outlives the plugin.
3012 - if ( 'no' === get_transient( self::NO_IMPORTED_FORMS_TRANSIENT ) ) {
3013 - return [];
3014 - }
3015 -
3016 - // Only forms imported from a Starter Templates (Astra Sites) starter
3017 - // template — see self::ASTRA_SITES_IMPORT_META. Prime post + meta caches
3018 - // (the loop reads meta, title and creation time per candidate) so this is a
3019 - // single query rather than the main query plus a follow-up per form.
3020 - $query = new \WP_Query(
3021 - [
3022 - 'post_type' => SRFM_FORMS_POST_TYPE,
3023 - 'post_status' => 'publish',
3024 - 'posts_per_page' => 10,
3025 - // ID breaks the tie — an import creates several forms in the same
3026 - // second, so post_date alone makes "newest" MySQL-dependent.
3027 - 'orderby' => [
3028 - 'date' => 'DESC',
3029 - 'ID' => 'DESC',
3030 - ],
3031 - 'no_found_rows' => true,
3032 - 'update_post_meta_cache' => true,
3033 - 'update_post_term_cache' => false,
3034 - 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Bounded to 10 recent forms; admin-notice only.
3035 - [
3036 - 'key' => self::ASTRA_SITES_IMPORT_META,
3037 - 'compare' => 'EXISTS',
3038 - ],
3039 - ],
3040 - ]
3041 - );
3042 -
3043 - // Nothing on this site carries the marker — remember that, so the query does
3044 - // not repeat on every load. Keyed on the query result rather than on anything
3045 - // user-specific, so it is safe to share, and invalidated the moment a post is
3046 - // stamped (see invalidate_starter_template_cache()).
3047 - if ( empty( $query->posts ) ) {
3048 - set_transient( self::NO_IMPORTED_FORMS_TRANSIENT, 'no', WEEK_IN_SECONDS );
3049 - }
3050 -
3051 - $prompts = [];
3052 - $now = time();
3053 -
3054 - foreach ( $query->posts as $post ) {
3055 - $form_id = (int) $post->ID;
3056 -
3057 - if ( ! current_user_can( 'edit_post', $form_id ) ) {
3058 - continue;
3059 - }
3060 -
3061 - $steps = [
3062 - // A destination for replies: an enabled notification with a recipient.
3063 - 'replies' => ! self::form_has_reply_destination( $form_id ),
3064 - // The thank-you message is still the shipped default.
3065 - 'thankyou' => self::is_default_confirmation_message( $form_id ),
3066 - ];
3067 -
3068 - // Nothing left to finish — no card for this form.
3069 - if ( ! $steps['replies'] && ! $steps['thankyou'] ) {
3070 - continue;
3071 - }
3072 -
3073 - $edit_link = get_edit_post_link( $form_id, 'raw' );
3074 -
3075 - if ( empty( $edit_link ) ) {
3076 - continue;
3077 - }
3078 -
3079 - $created = get_post_time( 'U', true, $form_id );
3080 - $days_ago = is_int( $created ) ? (int) floor( ( $now - $created ) / DAY_IN_SECONDS ) : 0;
3081 -
3082 - $prompts[] = [
3083 - 'id' => $form_id,
3084 - 'title' => get_the_title( $form_id ),
3085 - 'days_ago' => max( 0, $days_ago ),
3086 - 'steps' => $steps,
3087 - 'edit_url' => $edit_link,
3088 - // The editor reads srfm_focus to open the matching settings tab:
3089 - // "notifications" lands on Email Notification (where the reply
3090 - // destination is set, so the CTA can actually clear that step) and
3091 - // "thankyou" on Form Confirmation.
3092 - 'replies_url' => add_query_arg( 'srfm_focus', 'notifications', $edit_link ),
3093 - 'thankyou_url' => add_query_arg( 'srfm_focus', 'thankyou', $edit_link ),
3094 - ];
3095 -
3096 - // One card is enough — surface only the latest form needing setup.
3097 - break;
3098 - }
3099 -
3100 - return $prompts;
3101 - }
3102 -
3103 - /**
3104 - * Build the Thank You notice's inner markup (title, sentence, action buttons).
3105 - *
3106 - * @param array<string,mixed> $form Prompt payload from get_thankyou_prompt_forms().
3107 - *
3108 - * @since 2.12.4
3109 - * @return string
3110 - */
3111 - private static function build_thankyou_notice_markup( $form ) {
3112 - // The prompt only surfaces starter-template imports (see the meta gate), so
3113 - // the form was created for the user rather than by them. Kept generic — no
3114 - // per-step claim — so it is always accurate whatever the user has since
3115 - // changed, while the action buttons point to the specific things to finish.
3116 - $sentence = __( 'We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions.', 'sureforms' );
3117 -
3118 - ob_start();
3119 - ?>
3120 - <p class="srfm-thankyou-notice__title">
3121 - <?php
3122 - echo esc_html(
3123 - sprintf(
3124 - /* translators: %s: form name. */
3125 - __( 'Finish setting up “%s”', 'sureforms' ),
3126 - $form['title']
3127 - )
3128 - );
3129 - ?>
3130 - </p>
3131 - <p class="srfm-thankyou-notice__text"><?php echo esc_html( $sentence ); ?></p>
3132 - <p class="srfm-thankyou-notice__actions">
3133 - <a class="button button-primary srfm-ty-edit-form" href="<?php echo esc_url( $form['edit_url'] ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Edit form', 'sureforms' ); ?></a>
3134 - <a class="button srfm-ty-edit-thankyou" href="<?php echo esc_url( $form['thankyou_url'] ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Edit the Thank You message', 'sureforms' ); ?></a>
3135 - <a class="button srfm-ty-set-replies" href="<?php echo esc_url( $form['replies_url'] ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Set where replies go', 'sureforms' ); ?></a>
3136 - </p>
3137 - <?php
3138 - return (string) ob_get_clean();
3139 - }
3140 -
3141 - /**
3142 - * Determine whether a notice callback is owned by SureForms.
3143 - *
3144 - * Recognises object methods on classes in the `SRFM` / `SRFM_PRO` namespaces
3145 - * as well as the bundled notices libraries (`BSF_Admin_Notices` and the
3146 - * legacy `Astra_Notices` alias). Everything else is treated as foreign.
3147 - *
3148 - * @param callable|array|string|null $function The registered callback function.
3149 - * @since 2.10.0
3150 - * @return bool True when the callback belongs to SureForms, false otherwise.
3151 - */
3152 - private function is_sureforms_owned_notice_callback( $function ) {
3153 - $class_name = '';
3154 -
3155 - if ( is_array( $function ) && isset( $function[0] ) ) {
3156 - // Object or static method callback represented as an array. The first
3157 - // element is either the object instance or the fully qualified class name.
3158 - $class_name = is_object( $function[0] ) ? get_class( $function[0] ) : (string) $function[0];
3159 - } elseif ( is_string( $function ) && false !== strpos( $function, '::' ) ) {
3160 - // Static method passed as "Class::method".
3161 - $class_name = strstr( $function, '::', true );
3162 - }
3163 -
3164 - if ( '' === $class_name ) {
3165 - // Plain function callbacks are never owned by SureForms.
3166 - return false;
3167 - }
3168 -
3169 - // SureForms (free and pro) namespaced classes. Pro's real namespace is
3170 - // `SRFM_Pro\` (case-sensitive) — not the all-caps `SRFM_PRO_` constant
3171 - // prefix — so match it case-insensitively to be safe.
3172 - if ( 0 === strpos( $class_name, 'SRFM\\' ) || 0 === stripos( $class_name, 'SRFM_Pro\\' ) ) {
3173 - return true;
3174 - }
3175 -
3176 - // Bundled notices library shipped with SureForms.
3177 - return in_array( $class_name, [ 'BSF_Admin_Notices', 'Astra_Notices' ], true );
3178 - }
3179 -
3180 - /**
3181 - * Whether the current admin page is a SureForms-owned screen identified by a
3182 - * `sureforms_*` / `srfm_*` `page` query slug. Complements
3183 - * {@see Helper::is_sureforms_admin_page()} so foreign-notice suppression also
3184 - * covers the payments / quiz / survey / learn / SMTP / partial-entries screens
3185 - * that the core helper does not enumerate. Read-only screen check.
3186 - *
3187 - * @since 2.10.0
3188 - * @return bool
3189 - */
3190 - private function is_sureforms_owned_admin_page() {
3191 - if ( ! is_admin() || empty( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection, no state change.
3192 - return false;
3193 - }
3194 - $page = sanitize_key( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection, no state change.
3195 - return 0 === strpos( $page, 'sureforms' ) || 0 === strpos( $page, 'srfm' );
3196 - }
3197 -
3198 - /**
3199 - * Build the shared HTML markup for admin notices.
3200 - *
3201 - * @since 2.5.2
3202 - *
3203 - * All text parameters must be pre-escaped by the caller (e.g. via esc_html__()).
3204 - * URL parameters must be pre-escaped via esc_url().
3205 - *
3206 - * @param string $heading The notice heading text (pre-escaped).
3207 - * @param string $message The notice body text (pre-escaped).
3208 - * @param string $cta_url The primary CTA URL (pre-escaped).
3209 - * @param string $cta_text The primary CTA button text (pre-escaped).
3210 - * @param string $snooze_text The snooze button text (pre-escaped).
3211 - * @param string $dismiss_text The dismiss button text (pre-escaped).
3212 - * @param int $snooze_duration Snooze duration in seconds for the data-repeat-notice-after attribute.
3213 - * @param bool $external_cta Whether the CTA opens in a new tab and also dismisses the notice
3214 - * via the astra-notice-close class. Default false.
3215 - * @return string The notice HTML markup.
3216 - */
3217 - private function build_notice_markup( $heading, $message, $cta_url, $cta_text, $snooze_text, $dismiss_text, $snooze_duration, $external_cta = false ) {
3218 - $image_path = esc_url( SRFM_URL . 'admin/assets/sureforms-logo.png' );
3219 - $cta_class = $external_cta ? 'astra-notice-close button-primary' : 'button-primary';
3220 - $cta_attrs = $external_cta ? ' target="_blank" rel="noopener noreferrer"' : '';
3221 -
3222 - return sprintf(
3223 - '<div class="notice-image">
3224 - <img src="%1$s" class="custom-logo" alt="SureForms" itemprop="logo">
3225 - </div>
3226 - <div class="notice-content">
3227 - <div class="notice-heading">
3228 - %2$s
3229 - </div>
3230 - %3$s<br />
3231 - <div class="astra-review-notice-container">
3232 - <a href="%4$s" class="%5$s"%6$s>
3233 - %7$s
3234 - </a>
3235 - <span class="dashicons dashicons-clock" aria-hidden="true"></span>
3236 - <a href="#" data-repeat-notice-after="%8$s" class="astra-notice-close">
3237 - %9$s
3238 - </a>
3239 - <span class="dashicons dashicons-smiley" aria-hidden="true"></span>
3240 - <a href="#" class="astra-notice-close">
3241 - %10$s
3242 - </a>
3243 - </div>
3244 - </div>',
3245 - $image_path,
3246 - $heading,
3247 - $message,
3248 - $cta_url,
3249 - esc_attr( $cta_class ),
3250 - $cta_attrs,
3251 - $cta_text,
3252 - $snooze_duration,
3253 - $snooze_text,
3254 - $dismiss_text
3255 - );
3256 - }
3257 -
3258 - /**
3259 - * Callback for displaying the rating notice conditionally.
3260 - *
3261 - * Returns true if the user has 3 or more published forms or 3 or more form entries.
3262 - *
3263 - * @since 2.5.2
3264 - * @return bool
3265 - */
3266 - private function maybe_display_rating_notice() {
3267 - if ( null === $this->should_show_rating ) {
3268 - $entries_count = Entries::get_total_entries_by_status( 'all' );
3269 - $form_count = wp_count_posts( SRFM_FORMS_POST_TYPE );
3270 - $this->should_show_rating = $entries_count >= self::RATING_NOTICE_THRESHOLD || Helper::get_integer_value( $form_count->publish ?? 0 ) >= self::RATING_NOTICE_THRESHOLD;
3271 - }
3272 -
3273 - return $this->should_show_rating;
3274 - }
3275 -
3276 - /**
3277 - * Get random premium feature text.
3278 - *
3279 - * @return string Random feature text.
3280 - * @since 1.9.1
3281 - */
3282 - private function get_random_premium_feature_text() {
3283 - $features = [
3284 - __( 'Use Conditional Logic to show only what matters', 'sureforms' ),
3285 - __( 'Split your form into steps to keep it easy', 'sureforms' ),
3286 - __( 'Let people upload files directly to your form', 'sureforms' ),
3287 - __( 'Turn responses into downloadable PDFs automatically', 'sureforms' ),
3288 - __( 'Let users sign with a simple signature field', 'sureforms' ),
3289 - __( 'Connect your form to other tools using webhooks', 'sureforms' ),
3290 - __( 'Use Conversational Forms for a chat-like experience', 'sureforms' ),
3291 - __( 'Let users register or log in through your form', 'sureforms' ),
3292 - __( 'Build forms that create WordPress user accounts', 'sureforms' ),
3293 - __( 'Add calculations to auto-total scores or prices', 'sureforms' ),
3294 - ];
3295 -
3296 - // Get a random feature.
3297 - $random_key = array_rand( $features );
3298 - return $features[ $random_key ];
3299 - }
3300 -
3301 - /**
3302 - * Render the dashboard widget footer for upsell.
3303 - *
3304 - * @param array $entries_data The entries data array.
3305 - * @return void
3306 - * @since 1.9.1
3307 - */
3308 - private function render_dashboard_widget_footer( $entries_data ) {
3309 - // Only show footer if Pro is not active.
3310 - if ( Helper::has_pro() ) {
3311 - return;
3312 - }
3313 -
3314 - // Count total entries in last 7 days.
3315 - $total_entries = 0;
3316 - foreach ( $entries_data as $form_data ) {
3317 - $total_entries += $form_data['count'];
3318 - }
3319 -
3320 - // Count total published forms.
3321 - $published_forms_count = wp_count_posts( SRFM_FORMS_POST_TYPE )->publish;
3322 -
3323 - // Show footer only if 3+ entries received OR 3+ forms published.
3324 - if ( $total_entries >= 3 || $published_forms_count >= 3 ) {
3325 - ?>
3326 - <div class="srfm-widget-footer">
3327 - <div class="srfm-upgrade-content">
3328 - <svg class="srfm-logo-icon" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
3329 - <rect width="20" height="20" fill="#D54407"/>
3330 - <path d="M5.7139 4.2854H14.2853V7.1425H7.1424L5.7139 8.5711V7.1425V4.2854Z" fill="white"/>
3331 - <path d="M5.7139 4.2854H14.2853V7.1425H7.1424L5.7139 8.5711V7.1425V4.2854Z" fill="white"/>
3332 - <path d="M5.7148 8.5713H12.8577V11.4284H7.1434L5.7148 12.857V11.4284V8.5713Z" fill="white"/>
3333 - <path d="M5.7148 8.5713H12.8577V11.4284H7.1434L5.7148 12.857V11.4284V8.5713Z" fill="white"/>
3334 - <path d="M5.7148 12.8569H10.0006V15.7141H5.7148V12.8569Z" fill="white"/>
3335 - <path d="M5.7148 12.8569H10.0006V15.7141H5.7148V12.8569Z" fill="white"/>
3336 - </svg>
3337 - <span><?php echo esc_html( $this->get_random_premium_feature_text() ); ?></span>
3338 - </div>
3339 - <?php
3340 - $upgrade_url = Helper::get_sureforms_website_url( 'pricing', [ 'utm_medium' => 'dashboard-widget' ] );
3341 - ?>
3342 - <a href="<?php echo esc_url( $upgrade_url ); ?>" class="srfm-upgrade-link" target="_blank">
3343 - <?php esc_html_e( 'Upgrade', 'sureforms' ); ?>
3344 - </a>
3345 - </div>
3346 - <?php
3347 - }
3348 - }
3349 -
3350 - /**
3351 - * Determine if the admin pointer should be visible on this page.
3352 - *
3353 - * @since 1.8.0
3354 - * @return bool
3355 - */
3356 - private function is_admin_pointer_visible() {
3357 - global $pagenow;
3358 - $allowed_pages = [ 'index.php', 'options-general.php' ];
3359 -
3360 - // Do not show if pointer dismissed, accepted, or more than 1 form exists.
3361 - if (
3362 - ! empty( Helper::get_srfm_option( 'pointer_popup_dismissed' ) )
3363 - || ! empty( Helper::get_srfm_option( 'pointer_popup_accepted' ) )
3364 - || (int) ( wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0 ) > 1
3365 - ) {
3366 - return false;
3367 - }
3368 -
3369 - if ( in_array( $pagenow, $allowed_pages, true ) ) {
3370 - return true;
3371 - }
3372 -
3373 - return false;
3374 - }
3375 -
3376 867 }