PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.3
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.3
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
← All changes | includes/core/Tracking/class-wpfnl-telemetry.php +538 -21 3.12.2 → 3.13.3 View file →
@@ -78,8 +78,15 @@
78 78 return;
79 79 }
80 80
81 81 $this->register_triggers();
82 + $this->register_retention_events();
83 +
84 + /*
85 + * Product activity trail: every screen and REST action, sent daily for
86 + * opted-in sites and at deactivation for every site.
87 + */
88 + ActivityTrail::init( $this->client, self::POSTHOG_API_KEY );
82 89 }
83 90
84 91 /**
85 92 * Wire up all WordPress hooks for event tracking.
@@ -111,8 +118,47 @@
111 118 'vertical_canvas_mode' => [
112 119 'hook' => 'wpfunnels_vertical_mode_used',
113 120 'callback' => [ $this, 'feature_usage_payload' ],
114 121 ],
122 + // Retention signals — low-frequency admin or AI-driven actions, sent on every use.
123 + // template_funnel_imported — a funnel was built from the template library.
124 + 'template_funnel_imported' => [
125 + 'hook' => 'wpfunnels_after_funnel_imported',
126 + 'callback' => function ( $funnel_id = 0, $type = '', $builder = '' ) {
127 + return [
128 + 'template_type' => sanitize_key( (string) $type ),
129 + 'builder' => sanitize_key( (string) $builder ),
130 + ];
131 + },
132 + ],
133 + // ai_chat_started — the AI assistant was opened for real work.
134 + 'ai_chat_started' => [
135 + 'hook' => 'wpfunnels_ai_chat_started',
136 + 'callback' => function ( $conversation_id = 0, $user_id = 0, $context_type = '' ) {
137 + return [ 'context_type' => sanitize_key( (string) $context_type ) ];
138 + },
139 + ],
140 + // ai_funnel_published — a funnel built by the AI assistant went live.
141 + 'ai_funnel_published' => [
142 + 'hook' => 'wpfunnels_ai_funnel_published',
143 + 'callback' => function () {
144 + return [];
145 + },
146 + ],
147 + // mail_mint_automation_saved — email automation attached to a funnel.
148 + 'mail_mint_automation_saved' => [
149 + 'hook' => 'wpfunnels/save_mint_automation',
150 + 'callback' => function () {
151 + return [];
152 + },
153 + ],
154 + // step_duplicated — a step was duplicated on the canvas.
155 + 'step_duplicated' => [
156 + 'hook' => 'wpfunnels/after_step_duplicate',
157 + 'callback' => function () {
158 + return [];
159 + },
160 + ],
115 161 ],
116 162 ] );
117 163 } catch ( \Exception $e ) {
118 164 // Failure-safe: trigger registration errors must not surface to callers.
@@ -138,11 +184,227 @@
138 184 add_action( 'wpfunnels/funnel_order_placed', [ $this, 'handle_aha_milestone' ], 10, 3 );
139 185
140 186 // Replace generic SDK deactivation reasons with WPFunnels-specific ones.
141 187 add_filter( 'wpfunnels_telemetry_deactivation_reasons', [ $this, 'filter_deactivation_reasons' ] );
188 +
189 + // Swap the SDK's shared deactivation modal for WPFunnels' own isolated implementation.
190 + // The SDK's modal uses generic CSS class names (wd-dr-*, wd-de-*) that conflict with
191 + // mail-mint and wpvr when all plugins are active. WPFunnels' modal uses wpfnl-dr-/wpfnl-de-
192 + // prefixed classes and one-click behavior, keeping it fully independent.
193 + //
194 + // We cannot call $this->client->get_deactivation() because another plugin (mail-mint,
195 + // wpvr) may load its own Client.php first — only one class definition wins in PHP.
196 + // Instead we scan $wp_filter at priority 0 and identify WPFunnels' Deactivation
197 + // instance by the plugin_action_links hook it registers for WPFNL_FILE.
198 + add_action( 'admin_footer', [ $this, 'remove_sdk_deactivation_modal' ], 0 );
199 + add_action( 'admin_footer', [ $this, 'render_deactivation_modal' ] );
142 200 }
143 201
144 202 // -------------------------------------------------------------------------
203 + // Retention: weekly "value delivered" signals
204 + // -------------------------------------------------------------------------
205 +
206 + /**
207 + * Is WPFunnels producing results on this site this week?
208 + *
209 + * Every hook below fires on visitor or cron traffic (checkouts, offers,
210 + * opt-ins), so each feature is sent at most once a week per site through
211 + * the weekly gate. Properties are non-personal: step or offer type and a
212 + * value bucket — never order, customer or funnel ids.
213 + *
214 + * Hooks were chosen by their call sites, not their names. Left out on
215 + * purpose: `wpfunnels/update_ab_testing_winner` (fires on every Pro step
216 + * visit, not when a winner is chosen), `wpfunnels/ab_testing_auto_end`
217 + * (fires when the auto-end setting is saved) and `wpfunnels/trigger_automation`
218 + * (fires at the end of every journey whether or not an automation exists).
219 + */
220 + private function register_retention_events(): void {
221 + // funnel_order_placed — a WooCommerce order was placed through a funnel checkout.
222 + add_action(
223 + 'wpfunnels/funnel_order_placed',
224 + function ( $order_id = 0 ) {
225 + $this->send_weekly_feature_used(
226 + 'funnel_order_placed',
227 + function () use ( $order_id ) {
228 + $order = function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : null;
229 + return [ 'value_bucket' => $order ? self::value_bucket( $order->get_total() ) : 'unknown' ];
230 + }
231 + );
232 + },
233 + 10,
234 + 1
235 + );
236 +
237 + // order_bump_accepted — a shopper ticked an order bump at checkout (before the order).
238 + add_action(
239 + 'wpfunnels/order_bump_accepted',
240 + function () {
241 + $this->send_weekly_feature_used( 'order_bump_accepted' );
242 + }
243 + );
244 +
245 + // offer_accepted / offer_rejected — an upsell or downsell was answered.
246 + foreach ( [ 'wpfunnels/offer_accepted' => 'offer_accepted', 'wpfunnels/offer_rejected' => 'offer_rejected' ] as $hook => $feature ) {
247 + add_action(
248 + $hook,
249 + function ( $order = null, $offer_product = [] ) use ( $feature ) {
250 + $this->send_weekly_feature_used(
251 + $feature,
252 + function () use ( $offer_product ) {
253 + $step_id = is_array( $offer_product ) && isset( $offer_product['step_id'] ) ? absint( $offer_product['step_id'] ) : 0;
254 + $properties = [ 'offer_type' => $step_id ? sanitize_key( (string) get_post_meta( $step_id, '_step_type', true ) ) : 'unknown' ];
255 + if ( is_array( $offer_product ) && isset( $offer_product['total'] ) && is_numeric( $offer_product['total'] ) ) {
256 + $properties['value_bucket'] = self::value_bucket( $offer_product['total'] );
257 + }
258 + return $properties;
259 + }
260 + );
261 + },
262 + 10,
263 + 2
264 + );
265 + }
266 +
267 + // optin_submitted — a visitor submitted a funnel opt-in form.
268 + add_action(
269 + 'wpfunnels/after_optin_submit',
270 + function ( $step_id = 0, $post_action = '' ) {
271 + $this->send_weekly_feature_used( 'optin_submitted', [ 'post_action' => sanitize_key( (string) $post_action ) ] );
272 + },
273 + 10,
274 + 2
275 + );
276 +
277 + // funnel_visited / funnel_completed — a visitor entered a funnel's first
278 + // step or reached its last one. Admins previewing their own funnels are skipped.
279 + foreach ( [ 'wpfunnels/funnel_journey_starts' => 'funnel_visited', 'wpfunnels/funnel_journey_end' => 'funnel_completed' ] as $hook => $feature ) {
280 + add_action(
281 + $hook,
282 + function () use ( $feature ) {
283 + if ( current_user_can( 'wpf_manage_funnels' ) ) {
284 + return;
285 + }
286 + $this->send_weekly_feature_used( $feature );
287 + }
288 + );
289 + }
290 +
291 + // checkout_visit_recorded — a unique visitor reached a funnel or store checkout.
292 + add_action(
293 + 'wpfunnels/checkout_visit_recorded',
294 + function ( $type = '' ) {
295 + $this->send_weekly_feature_used( 'checkout_visit_recorded', [ 'checkout_type' => sanitize_key( (string) $type ) ] );
296 + }
297 + );
298 +
299 + // offer_subscription_created — an offer created a WooCommerce subscription (Pro).
300 + add_action(
301 + 'wpfunnels/subscription_created',
302 + function () {
303 + $this->send_weekly_feature_used( 'offer_subscription_created' );
304 + }
305 + );
306 + }
307 +
308 + /**
309 + * Send a `feature_used` event at most once a week per site.
310 + *
311 + * These hooks sit on hot visitor paths, so the two cheap checks come first:
312 + * not opted in (track_feature_used() would drop the event anyway) and
313 + * already sent this week. Only then does claim_weekly_slot() take the
314 + * atomic slot. Properties may be a callable, evaluated only when the event
315 + * is actually sent.
316 + *
317 + * @param string $feature Feature name.
318 + * @param array|callable $properties Event properties, or a callable returning them.
319 + * @return void
320 + */
321 + public function send_weekly_feature_used( string $feature, $properties = [] ): void {
322 + try {
323 + if ( ! $this->client || 'yes' !== $this->client->get_optin_state() ) {
324 + return;
325 + }
326 +
327 + if ( ! self::claim_weekly_slot( 'wpfunnels_telemetry_' . $feature . '_last_sent' ) ) {
328 + return;
329 + }
330 +
331 + $properties = is_callable( $properties ) ? call_user_func( $properties ) : $properties;
332 + $this->client->track_feature_used( $feature, is_array( $properties ) ? $properties : [] );
333 + } catch ( \Throwable $e ) {
334 + // Telemetry must never break a checkout or an offer.
335 + }
336 + }
337 +
338 + /**
339 + * Claim this week's send slot for a gate option.
340 + *
341 + * Fast path: already sent this week — skips the write for almost every
342 + * call; a stale read just falls through to the atomic claim. The claim is a
343 + * conditional UPDATE, so only one of several concurrent requests can flip
344 + * the value; the rest affect 0 rows.
345 + *
346 + * @param string $option_key Gate option storing the last-sent time.
347 + * @param int|null $now Current time (for tests).
348 + * @return bool True when this call won the slot.
349 + */
350 + public static function claim_weekly_slot( string $option_key, ?int $now = null ): bool {
351 + global $wpdb;
352 +
353 + $now = null === $now ? time() : $now;
354 + $threshold = $now - WEEK_IN_SECONDS;
355 +
356 + if ( (int) get_option( $option_key, 0 ) >= $threshold ) {
357 + return false;
358 + }
359 +
360 + // Ensure the row exists so the conditional UPDATE has something to claim.
361 + // add_option is a no-op if it already exists, preserving the last send time.
362 + add_option( $option_key, '0', '', false );
363 +
364 + $claimed = $wpdb->query(
365 + $wpdb->prepare(
366 + "UPDATE {$wpdb->options} SET option_value = %d WHERE option_name = %s AND option_value < %d",
367 + $now,
368 + $option_key,
369 + $threshold
370 + )
371 + ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- atomic claim; the options API check-then-set is racy under concurrency.
372 +
373 + if ( empty( $claimed ) ) {
374 + return false;
375 + }
376 +
377 + // Keep the options cache in sync with the value just written directly.
378 + wp_cache_delete( $option_key, 'options' );
379 +
380 + return true;
381 + }
382 +
383 + /**
384 + * Bucket a money amount so no exact order value is sent. Store currency
385 + * is not converted; the buckets only tell small from large orders.
386 + *
387 + * @param mixed $amount Amount.
388 + * @return string
389 + */
390 + public static function value_bucket( $amount ): string {
391 + $amount = (float) $amount;
392 +
393 + if ( $amount <= 0 ) {
394 + return '0';
395 + }
396 +
397 + foreach ( [ 50 => '1-49', 100 => '50-99', 250 => '100-249', 500 => '250-499', 1000 => '500-999' ] as $limit => $bucket ) {
398 + if ( $amount < $limit ) {
399 + return $bucket;
400 + }
401 + }
402 +
403 + return '1000+';
404 + }
405 +
406 + // -------------------------------------------------------------------------
145 407 // Public API — called from REST controllers
146 408 // -------------------------------------------------------------------------
147 409
148 410 /**
@@ -160,8 +422,48 @@
160 422 $this->client->set_optin_state( $consented ? 'yes' : 'no' );
161 423 }
162 424
163 425 /**
426 + * Read the stored consent decision.
427 + *
428 + * Delegates to the SDK, which also migrates the pre-Linno Appsero keys
429 + * (`wpfunnels_allow_tracking` and friends) onto the shared Linno option — so
430 + * a site that consented through an older setup wizard still reports 'yes'
431 + * here. When the SDK is unavailable we read those same keys directly rather
432 + * than reporting "no decision" and silently losing an existing opt-in.
433 + *
434 + * @return string|null 'yes', 'no', or null when the user has never decided.
435 + */
436 + public function get_consent_state(): ?string {
437 + if ( $this->client ) {
438 + return $this->client->get_optin_state();
439 + }
440 +
441 + foreach ( [ 'linno_telemetry_allow_tracking', WPFNL_SLUG . '_allow_tracking' ] as $option_key ) {
442 + $value = get_option( $option_key, null );
443 +
444 + if ( is_string( $value ) ) {
445 + $value = strtolower( trim( $value ) );
446 +
447 + if ( 'yes' === $value || 'no' === $value ) {
448 + return $value;
449 + }
450 + }
451 + }
452 +
453 + return null;
454 + }
455 +
456 + /**
457 + * Whether the user has actively opted in to usage tracking.
458 + *
459 + * @return bool
460 + */
461 + public function has_consent(): bool {
462 + return 'yes' === $this->get_consent_state();
463 + }
464 +
465 + /**
164 466 * Dispatch a step-view or step-completion event from the REST layer.
165 467 *
166 468 * @param array $data Keys: event_type (viewed|completed), step_name, step_index, goal, time_on_step.
167 469 */
@@ -512,13 +814,232 @@
512 814 // Deactivation feedback
513 815 // -------------------------------------------------------------------------
514 816
515 817 /**
818 + * Find and remove the SDK's admin_footer deactivation modal for WPFunnels.
819 + *
820 + * Runs at priority 0 so it fires before the SDK's default priority-10 callback.
821 + * We cannot use get_deactivation() on Client because another plugin may have
822 + * loaded its own Client.php first (PHP only keeps the first class definition).
823 + * Instead we identify WPFunnels' Deactivation instance by the plugin_action_links
824 + * hook it uniquely registers for WPFNL_FILE, then remove its admin_footer callback.
825 + */
826 + public function remove_sdk_deactivation_modal() {
827 + global $wp_filter;
828 +
829 + if ( ! isset( $wp_filter['admin_footer'] ) ) {
830 + return;
831 + }
832 +
833 + $links_hook = 'plugin_action_links_' . plugin_basename( WPFNL_FILE );
834 +
835 + foreach ( $wp_filter['admin_footer']->callbacks as $priority => $callbacks ) {
836 + foreach ( $callbacks as $cb ) {
837 + $fn = $cb['function'];
838 + if ( ! is_array( $fn ) || ! ( $fn[0] instanceof \LinnoSDK\Telemetry\Deactivation ) ) {
839 + continue;
840 + }
841 + if ( has_filter( $links_hook, [ $fn[0], 'filter_plugin_action_links' ] ) ) {
842 + remove_action( 'admin_footer', [ $fn[0], 'add_deactivation_feedback_modal' ], $priority );
843 + return;
844 + }
845 + }
846 + }
847 + }
848 +
849 + /**
850 + * Render WPFunnels' own deactivation modal on the plugins.php page.
851 + *
852 + * This replaces the SDK's generic modal (unhooked via remove_sdk_deactivation_modal()) so
853 + * WPFunnels uses isolated wpfnl-dr-/wpfnl-de- CSS class names and one-click
854 + * UX without conflicting with mail-mint or wpvr modals on the same page.
855 + * The SDK's plugin_action_links filter and AJAX handler remain active.
856 + */
857 + public function render_deactivation_modal() {
858 + global $pagenow;
859 + if ( 'plugins.php' !== $pagenow ) {
860 + return;
861 + }
862 +
863 + $nonce = wp_create_nonce( 'wpfunnels-deactivation-nonce' );
864 + $reasons = apply_filters( 'wpfunnels_telemetry_deactivation_reasons', [] );
865 +
866 + if ( empty( $reasons ) ) {
867 + return;
868 + }
869 + ?>
870 + <style type="text/css">
871 + .wpfnl-dr-modal {
872 + position: fixed;
873 + z-index: 99999;
874 + top: 0; right: 0; bottom: 0; left: 0;
875 + background: rgba(0,0,0,0.5);
876 + display: none;
877 + box-sizing: border-box;
878 + overflow: scroll;
879 + }
880 + .wpfnl-dr-modal * { box-sizing: border-box; }
881 + .wpfnl-dr-modal.modal-active { display: block; }
882 + .wpfnl-dr-modal-wrap {
883 + max-width: 780px;
884 + width: 100%;
885 + position: relative;
886 + margin: 8% auto;
887 + background: #fff;
888 + border-radius: 8px;
889 + box-shadow: 0 8px 32px rgba(0,0,0,0.18);
890 + }
891 + .wpfnl-dr-modal-header {
892 + border-bottom: 1px solid #E8E8E8;
893 + padding: 24px 24px 16px 24px;
894 + }
895 + .wpfnl-dr-modal-header h3 {
896 + line-height: 1.5;
897 + margin: 0 0 4px 0;
898 + color: #1a1a2e;
899 + font-size: 16px;
900 + font-weight: 600;
901 + }
902 + .wpfnl-dr-modal-subheading { margin: 0; color: #718096; font-size: 13px; }
903 + .wpfnl-dr-modal-body { padding: 20px 24px 8px 24px; }
904 + .wpfnl-dr-modal-footer {
905 + border-top: 1px solid #E8E8E8;
906 + padding: 16px 24px;
907 + display: flex;
908 + align-items: center;
909 + justify-content: flex-end;
910 + }
911 + ul.wpfnl-de-reasons {
912 + display: flex;
913 + flex-wrap: wrap;
914 + margin: 0 -6px 12px -6px;
915 + padding: 0;
916 + list-style: none;
917 + }
918 + ul.wpfnl-de-reasons li { padding: 0 6px; margin: 0 0 12px 0; width: 25%; }
919 + .wpfnl-de-reason-btn {
920 + width: 100%;
921 + border: 1.5px solid #E8E8E8;
922 + border-radius: 6px;
923 + background: #fff;
924 + cursor: pointer;
925 + text-align: center;
926 + padding: 14px 8px 12px 8px;
927 + transition: border-color 0.15s, background 0.15s, transform 0.1s;
928 + display: flex;
929 + flex-direction: column;
930 + align-items: center;
931 + gap: 8px;
932 + height: 100%;
933 + }
934 + .wpfnl-de-reason-btn:hover {
935 + border-color: #7B68EE;
936 + background: #f5f3ff;
937 + transform: translateY(-1px);
938 + }
939 + .wpfnl-de-reason-btn:active { transform: translateY(0); }
940 + .wpfnl-de-reason-btn:disabled { cursor: default; opacity: 0.6; }
941 + .wpfnl-de-reason-text { color: #4A5568; font-size: 12px; line-height: 1.4; }
942 + .wpfnl-de-reason-icon { display: flex; align-items: center; justify-content: center; }
943 + ul.wpfnl-de-reasons li.wpfnl-de-reason-selected .wpfnl-de-reason-btn {
944 + background: #7B68EE;
945 + border-color: #7B68EE;
946 + }
947 + ul.wpfnl-de-reasons li.wpfnl-de-reason-selected .wpfnl-de-reason-icon svg,
948 + ul.wpfnl-de-reasons li.wpfnl-de-reason-selected .wpfnl-de-reason-icon svg g { fill: #fff; }
949 + ul.wpfnl-de-reasons li.wpfnl-de-reason-selected .wpfnl-de-reason-text { color: #fff; }
950 + .wpfnl-dr-btn-cancel,
951 + .wpfnl-dr-btn-cancel:hover {
952 + border: 1px solid #EBEBEB;
953 + border-radius: 4px;
954 + font-size: 13px;
955 + line-height: 1.5;
956 + color: #718096;
957 + padding: 6px 14px;
958 + cursor: pointer;
959 + background-color: transparent;
960 + text-decoration: none;
961 + }
962 + </style>
963 +
964 + <div class="wpfnl-dr-modal" id="wpfunnels-dr-modal">
965 + <div class="wpfnl-dr-modal-wrap">
966 + <div class="wpfnl-dr-modal-header">
967 + <h3><?php esc_html_e( "Quick question before you go — what's the main reason?", 'wpfnl' ); ?></h3>
968 + <p class="wpfnl-dr-modal-subheading"><?php esc_html_e( 'One click is all it takes. Your feedback helps us improve.', 'wpfnl' ); ?></p>
969 + </div>
970 + <div class="wpfnl-dr-modal-body">
971 + <ul class="wpfnl-de-reasons">
972 + <?php foreach ( $reasons as $reason ) : ?>
973 + <li data-reason-id="<?php echo esc_attr( $reason['id'] ); ?>">
974 + <button type="button" class="wpfnl-de-reason-btn">
975 + <div class="wpfnl-de-reason-icon"><?php echo $reason['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
976 + <div class="wpfnl-de-reason-text"><?php echo esc_html( $reason['text'] ); ?></div>
977 + </button>
978 + </li>
979 + <?php endforeach; ?>
980 + </ul>
981 + </div>
982 + <div class="wpfnl-dr-modal-footer">
983 + <button type="button" class="wpfnl-dr-btn-cancel"><?php esc_html_e( 'Cancel', 'wpfnl' ); ?></button>
984 + </div>
985 + </div>
986 + </div>
987 +
988 + <script type="text/javascript">
989 + (function($) {
990 + $(function() {
991 + var modal = $('#wpfunnels-dr-modal');
992 + var deactivateLink = '';
993 + var submitting = false;
994 +
995 + $('#the-list').on('click', 'a.wpfunnels-deactivation-link', function(e) {
996 + e.preventDefault();
997 + deactivateLink = $(this).attr('href');
998 + submitting = false;
999 + modal.find('.wpfnl-de-reason-btn').prop('disabled', false);
1000 + modal.find('li').removeClass('wpfnl-de-reason-selected');
1001 + modal.addClass('modal-active');
1002 + });
1003 +
1004 + modal.on('click', 'button.wpfnl-dr-btn-cancel', function(e) {
1005 + e.preventDefault();
1006 + modal.removeClass('modal-active');
1007 + });
1008 +
1009 + modal.on('click', '.wpfnl-de-reason-btn', function() {
1010 + if (submitting || !deactivateLink) return;
1011 + submitting = true;
1012 +
1013 + var reasonId = $(this).closest('li').data('reason-id');
1014 + $(this).closest('li').addClass('wpfnl-de-reason-selected');
1015 + modal.find('.wpfnl-de-reason-btn').prop('disabled', true);
1016 +
1017 + $.ajax({
1018 + url: ajaxurl,
1019 + type: 'POST',
1020 + data: {
1021 + nonce: '<?php echo esc_js( $nonce ); ?>',
1022 + action: 'wpfunnels_submit_deactivation_reason',
1023 + reason_id: reasonId,
1024 + reason_info: ''
1025 + },
1026 + complete: function() {
1027 + window.location.href = deactivateLink;
1028 + }
1029 + });
1030 + });
1031 + });
1032 + }(jQuery));
1033 + </script>
1034 + <?php
1035 + }
1036 +
1037 + /**
516 1038 * Replace the generic SDK deactivation reasons with WPFunnels-specific ones.
517 1039 *
518 - * Hooked on wpfunnels_telemetry_deactivation_reasons (fired by the SDK's
519 - * Deactivation class). Each reason requires only 'id', 'text', and 'icon'.
520 - * One-click on a card immediately submits and deactivates — no textarea needed.
1040 + * Hooked on wpfunnels_telemetry_deactivation_reasons. Called by render_deactivation_modal()
1041 + * via apply_filters to populate WPFunnels' own modal.
521 1042 *
522 1043 * @param array $reasons Default reasons from the SDK.
523 1044 * @return array
524 1045 */
@@ -523,9 +1044,20 @@
523 1044 * @return array
524 1045 */
525 1046 public function filter_deactivation_reasons( array $reasons ): array {
526 1047 return [
1048 +
527 1049 [
1050 + 'id' => 'plugin-conflict',
1051 + 'text' => __( 'Plugin conflict', 'wpfnl' ),
1052 + 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="#3B86FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1053 + ],
1054 + [
1055 + 'id' => 'payment-not-working',
1056 + 'text' => __( 'Payment not working', 'wpfnl' ),
1057 + 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="#3B86FF" stroke-width="2"/><path d="M12 7v5l3 3" stroke="#3B86FF" stroke-width="2" stroke-linecap="round"/></svg>',
1058 + ],
1059 + [
528 1060 'id' => 'funnel-builder-hard-to-use',
529 1061 'text' => __( 'Funnel builder too complex', 'wpfnl' ),
530 1062 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="3" stroke="#3B86FF" stroke-width="2"/><path d="M9 9h6M9 12h4" stroke="#3B86FF" stroke-width="2" stroke-linecap="round"/></svg>',
531 1063 ],
@@ -539,18 +1071,8 @@
539 1071 'text' => __( 'Upsell / downsell issues', 'wpfnl' ),
540 1072 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M3 17l4-4 4 4 4-8 4 4" stroke="#3B86FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
541 1073 ],
542 1074 [
543 - 'id' => 'plugin-conflict',
544 - 'text' => __( 'Plugin conflict', 'wpfnl' ),
545 - 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="#3B86FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
546 - ],
547 - [
548 - 'id' => 'payment-not-working',
549 - 'text' => __( 'Payment not working', 'wpfnl' ),
550 - 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="#3B86FF" stroke-width="2"/><path d="M12 7v5l3 3" stroke="#3B86FF" stroke-width="2" stroke-linecap="round"/></svg>',
551 - ],
552 - [
553 1075 'id' => 'missing-feature',
554 1076 'text' => __( 'Missing a feature I need', 'wpfnl' ),
555 1077 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="#3B86FF" stroke-width="2"/><path d="M12 8v4M12 16h.01" stroke="#3B86FF" stroke-width="2" stroke-linecap="round"/></svg>',
556 1078 ],
@@ -559,21 +1081,16 @@
559 1081 'text' => __( 'Templates are not good enough', 'wpfnl' ),
560 1082 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M5 12h14M12 5l7 7-7 7" stroke="#3B86FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
561 1083 ],
562 1084 [
563 - 'id' => 'missing-integration',
564 - 'text' => __( 'Missing integration I need', 'wpfnl' ),
565 - 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="#3B86FF" stroke-width="2" stroke-linecap="round"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="#3B86FF" stroke-width="2" stroke-linecap="round"/></svg>',
1085 + 'id' => 'lacking-payment-gateway',
1086 + 'text' => __( 'Lack of payment gateway options', 'wpfnl' ),
1087 + 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="#3B86FF" stroke-width="2"/><path d="M12 8v4M12 16h.01" stroke="#3B86FF" stroke-width="2" stroke-linecap="round"/></svg>',
566 1088 ],
567 1089 [
568 1090 'id' => 'found-better-plugin',
569 1091 'text' => __( 'Switching to another plugin', 'wpfnl' ),
570 1092 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M5 12h14M12 5l7 7-7 7" stroke="#3B86FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
571 - ],
572 - [
573 - 'id' => 'lacking-reporting',
574 - 'text' => __( 'Lack of reporting', 'wpfnl' ),
575 - 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="#3B86FF" stroke-width="2"/><path d="M12 8v4M12 16h.01" stroke="#3B86FF" stroke-width="2" stroke-linecap="round"/></svg>',
576 1093 ],
577 1094 [
578 1095 'id' => 'temporary',
579 1096 'text' => __( 'Temporarily deactivating', 'wpfnl' ),