PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / 3.3.3
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar v3.3.3
3.3.3 3.3.2 3.3.1 3.3.0 3.2.14 3.2.13 3.2.12 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 trunk 0.2.5.5 0.2.5.6 0.2.5.7 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 All 158 releases
← All changes | includes/Extensions/PopupNotification/PopupNotification.php +281 -7 3.2.10 → 3.3.3 View file →
@@ -126,9 +126,72 @@
126 126 'position' => 'center',
127 127 ],
128 128 'column' => "5",
129 129 ],
130 -
130 + // Coupon Popup card (light) — headline + description + copyable
131 + // coupon code + "Get Coupon" CTA + dismiss link.
132 + 'theme-eight' => [
133 + 'source' => NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/popup/popup-theme-eight.png',
134 + 'defaults' => [
135 + 'popup_title' => __('35% OFF NOW', 'notificationx'),
136 + 'popup_content' => __('Get an extra 35% off on all products!', 'notificationx'),
137 + 'popup_coupon_code' => __('GET35OFF', 'notificationx'),
138 + 'popup_button_text' => __('Get Coupon', 'notificationx'),
139 + 'popup_button_url' => '#',
140 + 'position' => 'center',
141 + ],
142 + 'column' => "5",
143 + ],
144 + // Festive/seasonal coupon popup — doodle-textured colored header
145 + // with a wavy divider, big discount headline, an eyebrow tagline,
146 + // a headline + copyable coupon code + soft dismiss link (no CTA).
147 + 'theme-eleven' => [
148 + 'source' => NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/popup/popup-theme-eleven.png?v=2',
149 + 'defaults' => [
150 + 'popup_subtitle' => __('Back to School Deal', 'notificationx'),
151 + 'popup_title' => __('25% OFF!', 'notificationx'),
152 + 'popup_content' => __('Get a ready-to-go healthy lunchbox kit', 'notificationx'),
153 + 'popup_coupon_code' => __('BTS2025', 'notificationx'),
154 + 'position' => 'center',
155 + ],
156 + 'column' => "5",
157 + ],
158 + // High-impact electronics flash-sale coupon popup — bold yellow
159 + // card, a rotated "%OFF" sticker badge (subtitle = the big number),
160 + // headline + subtext + copyable coupon code (no CTA / dismiss).
161 + 'theme-twelve' => [
162 + 'source' => NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/popup/popup-theme-twelve.png',
163 + 'defaults' => [
164 + 'popup_subtitle' => __('64%', 'notificationx'),
165 + 'popup_title' => __('Enjoy 64% Electronics Sale', 'notificationx'),
166 + 'popup_content' => __('Upgrade your gadgets at unbeatable prices.', 'notificationx'),
167 + 'popup_coupon_code' => __('ELEC 4512658', 'notificationx'),
168 + 'position' => 'center',
169 + ],
170 + 'column' => "5",
171 + ],
172 + // Multi-tier discount popup — gift hero, two-tone heading, a list of
173 + // coupon "tickets" (percent + code + validity) and a "Claim Now" CTA.
174 + 'theme-thirteen' => [
175 + 'source' => NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/popup/popup-theme-thirteen.png',
176 + 'defaults' => [
177 + 'popup_title' => __('Buy Upto $500', 'notificationx'),
178 + 'popup_subtitle' => __('And Get Discount Now', 'notificationx'),
179 + 'popup_content' => __('Take discount in your next order', 'notificationx'),
180 + 'popup_button_text' => __('Claim Now', 'notificationx'),
181 + 'popup_button_url' => '#',
182 + 'popup_coupon_repeater' => [
183 + [
184 + 'coupon_percent' => __('20%', 'notificationx'),
185 + 'coupon_code' => __('#SAVE20', 'notificationx'),
186 + 'coupon_validity' => __('Valid for 7 Days', 'notificationx'),
187 + ],
188 + ],
189 + 'position' => 'center',
190 + ],
191 + 'column' => "5",
192 + ],
193 +
131 194 ];
132 195 }
133 196
134 197 public function init_fields() {
@@ -583,10 +646,19 @@
583 646 'label' => __('Subtitle', 'notificationx'),
584 647 'name' => 'popup_subtitle',
585 648 'type' => 'text',
586 649 'priority' => 12,
587 - 'default' => __('Would like to get the latest news & updates instantly?', 'notificationx'),
588 - 'rules' => Rules::is('themes', 'popup_notification_theme-seven'),
650 + // No global field-level default: a hardcoded value here pre-fills the
651 + // field and blocks each theme's own `defaults` (subtitle) from applying
652 + // on selection, so coupon themes (eleven/twelve/thirteen) inherited the
653 + // wrong subtitle. Leave empty so the selected theme's default fills it.
654 + 'default' => '',
655 + 'rules' => Rules::logicalRule([
656 + Rules::is('themes', 'popup_notification_theme-seven'),
657 + Rules::is('themes', 'popup_notification_theme-eleven'),
658 + Rules::is('themes', 'popup_notification_theme-twelve'),
659 + Rules::is('themes', 'popup_notification_theme-thirteen'),
660 + ], 'or'),
589 661 ],
590 662 [
591 663 'label' => __('Popup Icon', 'notificationx'),
592 664 'name' => 'popup_icon',
@@ -611,10 +683,76 @@
611 683 'rules' => Rules::logicalRule([
612 684 Rules::is('themes', 'popup_notification_theme-one'),
613 685 Rules::is('themes', 'popup_notification_theme-two'),
614 686 Rules::is('themes', 'popup_notification_theme-seven'),
687 + Rules::is('themes', 'popup_notification_theme-eight'),
688 + Rules::is('themes', 'popup_notification_theme-eleven'),
689 + Rules::is('themes', 'popup_notification_theme-twelve'),
690 + Rules::is('themes', 'popup_notification_theme-thirteen'),
615 691 ], 'or'),
616 692 ],
693 + // Coupon code (with copy-to-clipboard) for the coupon-popup card
694 + // themes (theme-eight / -eleven / -twelve).
695 + [
696 + 'label' => __('Coupon Code', 'notificationx'),
697 + 'name' => 'popup_coupon_code',
698 + 'type' => 'text',
699 + 'priority' => 22,
700 + // No global field-level default: it pre-filled the field with theme-eight's
701 + // code and blocked theme-eleven/-twelve's own coupon-code defaults from
702 + // applying on selection. Leave empty so the selected theme's default fills it.
703 + 'default' => '',
704 + 'placeholder' => __('GET35OFF', 'notificationx'),
705 + 'rules' => Rules::logicalRule([
706 + Rules::is('themes', 'popup_notification_theme-eight'),
707 + Rules::is('themes', 'popup_notification_theme-eleven'),
708 + Rules::is('themes', 'popup_notification_theme-twelve'),
709 + ], 'or'),
710 + ],
711 + // Multi-tier coupon "tickets" (percent + code + validity) for the
712 + // multi-tier discount popup (theme-thirteen).
713 + [
714 + 'label' => __('Coupon Tickets', 'notificationx'),
715 + 'name' => 'popup_coupon_repeater',
716 + 'type' => 'repeater',
717 + 'priority' => 23,
718 + 'rules' => Rules::is('themes', 'popup_notification_theme-thirteen'),
719 + 'button' => [
720 + 'label' => __('Add Coupon', 'notificationx'),
721 + ],
722 + 'fields' => [
723 + [
724 + 'label' => __('Discount', 'notificationx'),
725 + 'name' => 'coupon_percent',
726 + 'type' => 'text',
727 + 'placeholder' => __('e.g. 20%', 'notificationx'),
728 + 'help' => __('Big discount value shown on the left (e.g. "20%").', 'notificationx'),
729 + ],
730 + [
731 + 'label' => __('Coupon Code', 'notificationx'),
732 + 'name' => 'coupon_code',
733 + 'type' => 'text',
734 + 'placeholder' => __('e.g. #SAVE20', 'notificationx'),
735 + ],
736 + [
737 + 'label' => __('Validity', 'notificationx'),
738 + 'name' => 'coupon_validity',
739 + 'type' => 'text',
740 + 'placeholder' => __('e.g. Valid for 7 Days', 'notificationx'),
741 + ],
742 + ],
743 + // One starter coupon by default. The QuickBuilder repeater always
744 + // renders a single row for an unsaved field, so a one-row default
745 + // keeps the editor, the saved data and the front end consistent
746 + // (all show one coupon); users add more with the Add Coupon button.
747 + 'default' => [
748 + [
749 + 'coupon_percent' => __('20%', 'notificationx'),
750 + 'coupon_code' => __('#SAVE20', 'notificationx'),
751 + 'coupon_validity' => __('Valid for 7 Days', 'notificationx'),
752 + ],
753 + ],
754 + ],
617 755 // Form Field Toggles - only for form submission themes (4-7)
618 756 [
619 757 'label' => __('Show Name Field', 'notificationx'),
620 758 'name' => 'popup_show_name_field',
@@ -705,9 +843,10 @@
705 843 Rules::is('popup_show_message_field', true),
706 844 ]),
707 845 ],
708 846
709 - // Common Button Text field for all themes
847 + // Common Button Text field for all themes (theme-eleven and
848 + // theme-twelve have no CTA — the copyable coupon code is the CTA).
710 849 [
711 850 'label' => __('Button Text', 'notificationx'),
712 851 'name' => 'popup_button_text',
713 852 'type' => 'text',
@@ -712,8 +851,12 @@
712 851 'name' => 'popup_button_text',
713 852 'type' => 'text',
714 853 'priority' => 55,
715 854 'default' => __('Get Offer', 'notificationx'),
855 + 'rules' => Rules::logicalRule([
856 + Rules::is('themes', 'popup_notification_theme-eleven', true),
857 + Rules::is('themes', 'popup_notification_theme-twelve', true),
858 + ], 'and'),
716 859 ],
717 860 // Button URL field - only for themes 1-3 (promotional themes with external links)
718 861 [
719 862 'label' => __('Button URL', 'notificationx'),
@@ -724,8 +867,10 @@
724 867 'rules' => Rules::logicalRule([
725 868 Rules::is('themes', 'popup_notification_theme-one'),
726 869 Rules::is('themes', 'popup_notification_theme-two'),
727 870 Rules::is('themes', 'popup_notification_theme-three'),
871 + Rules::is('themes', 'popup_notification_theme-eight'),
872 + Rules::is('themes', 'popup_notification_theme-thirteen'),
728 873 ], 'or'),
729 874 ],
730 875
731 876 // Button Icon field - only for theme-three and theme-seven
@@ -768,8 +913,10 @@
768 913 'rules' => Rules::logicalRule([
769 914 Rules::is('themes', 'popup_notification_theme-one'),
770 915 Rules::is('themes', 'popup_notification_theme-two'),
771 916 Rules::is('themes', 'popup_notification_theme-three'),
917 + Rules::is('themes', 'popup_notification_theme-eight'),
918 + Rules::is('themes', 'popup_notification_theme-thirteen'),
772 919 ], 'or'),
773 920 ],
774 921 // Repeater fields - only for theme-three
775 922 [
@@ -826,8 +973,134 @@
826 973 return $fields;
827 974 }
828 975
829 976 /**
977 + * Fill EMPTY theme-specific fields from the selected theme's `defaults`.
978 + *
979 + * Why this is needed: the builder applies a theme's `defaults` on selection, but
980 + * fields that are rules-gated OUT of the initial theme (e.g. popup_subtitle,
981 + * popup_coupon_code, popup_coupon_repeater) get re-initialized to their own
982 + * field-level default when they mount, clobbering the theme default. As a result
983 + * the coupon themes (eleven/twelve/thirteen) ended up with the wrong/empty
984 + * subtitle, coupon code and coupon tickets on a default (untouched) publish — so
985 + * both the live Preview and the front end didn't match the design.
986 + *
987 + * This safety net fills only EMPTY values, so anything the user actually typed is
988 + * preserved. A repeater row that holds only framework metadata (index/chosen/
989 + * selected) with no real content counts as empty.
990 + *
991 + * @param array $data Flat settings array (content fields at top level).
992 + * @param string $theme_full Selected theme key, e.g. "popup_notification_theme-twelve".
993 + * @return array
994 + */
995 + private function fill_theme_defaults( $data, $theme_full ) {
996 + if ( empty( $theme_full ) || empty( $this->themes ) || ! is_array( $data ) ) {
997 + return $data;
998 + }
999 + // Strip the source-id prefix ("popup_notification_") to get the raw theme key.
1000 + $prefix = $this->id . '_';
1001 + $theme_key = ( strpos( $theme_full, $prefix ) === 0 ) ? substr( $theme_full, strlen( $prefix ) ) : $theme_full;
1002 +
1003 + if ( empty( $this->themes[ $theme_key ]['defaults'] ) || ! is_array( $this->themes[ $theme_key ]['defaults'] ) ) {
1004 + return $data;
1005 + }
1006 + // Keys the repeater/field framework injects into each row that carry no
1007 + // user content — a row made up only of these is effectively empty.
1008 + $meta_keys = array( 'index', 'chosen', 'selected' );
1009 + foreach ( $this->themes[ $theme_key ]['defaults'] as $key => $value ) {
1010 + $current = isset( $data[ $key ] ) ? $data[ $key ] : null;
1011 + $is_empty = ( null === $current || '' === $current );
1012 + if ( is_array( $current ) ) {
1013 + // Empty array, or an array whose rows only hold framework metadata
1014 + // (e.g. a clobbered repeater like [{index, chosen, selected}]).
1015 + $has_real = false;
1016 + foreach ( $current as $item ) {
1017 + if ( is_array( $item ) ) {
1018 + foreach ( $item as $ik => $iv ) {
1019 + if ( ! in_array( $ik, $meta_keys, true ) && '' !== $iv && null !== $iv ) {
1020 + $has_real = true;
1021 + break 2;
1022 + }
1023 + }
1024 + } elseif ( '' !== $item && null !== $item ) {
1025 + $has_real = true;
1026 + break;
1027 + }
1028 + }
1029 + $is_empty = ! $has_real;
1030 + }
1031 + if ( $is_empty ) {
1032 + $data[ $key ] = $value;
1033 + }
1034 + }
1035 + return $data;
1036 + }
1037 +
1038 + /**
1039 + * Apply theme defaults to empty fields at save time (persisted + front end).
1040 + * Hooked automatically via nx_save_post_{$this->id} (see Extension::init()).
1041 + *
1042 + * @param array $post Post row about to be persisted; settings live in $post['data'].
1043 + * @param array $data Full submitted settings.
1044 + * @param int $nx_id Notification id (0 on create).
1045 + * @return array
1046 + */
1047 + public function save_post( $post, $data, $nx_id ) {
1048 + if ( isset( $post['data'] ) && is_array( $post['data'] ) ) {
1049 + $theme_full = ! empty( $data['themes'] ) ? $data['themes'] : ( ! empty( $post['theme'] ) ? $post['theme'] : '' );
1050 + $post['data'] = $this->fill_theme_defaults( $post['data'], $theme_full );
1051 + $post['data'] = $this->strip_empty_repeater_rows( $post['data'] );
1052 + }
1053 + return $post;
1054 + }
1055 +
1056 + /**
1057 + * Drop repeater rows that hold only framework metadata (index/chosen/selected)
1058 + * and no real content. The QuickBuilder repeater commits a freshly "Add"ed row
1059 + * with just an "index" until its inputs are edited, so an untouched extra coupon
1060 + * would otherwise persist as an empty ticket that renders as a broken blank
1061 + * ticket on the front end. Only the known coupon repeater is cleaned, so no other
1062 + * data is affected.
1063 + *
1064 + * @param array $data Flat settings array.
1065 + * @return array
1066 + */
1067 + private function strip_empty_repeater_rows( $data ) {
1068 + $meta_keys = array( 'index', 'chosen', 'selected' );
1069 + $repeater_fields = array( 'popup_coupon_repeater' );
1070 + foreach ( $repeater_fields as $field ) {
1071 + if ( empty( $data[ $field ] ) || ! is_array( $data[ $field ] ) ) {
1072 + continue;
1073 + }
1074 + $data[ $field ] = array_values( array_filter( $data[ $field ], function ( $row ) use ( $meta_keys ) {
1075 + if ( ! is_array( $row ) ) {
1076 + return '' !== $row && null !== $row;
1077 + }
1078 + foreach ( $row as $k => $v ) {
1079 + if ( ! in_array( $k, $meta_keys, true ) && '' !== $v && null !== $v ) {
1080 + return true;
1081 + }
1082 + }
1083 + return false;
1084 + } ) );
1085 + }
1086 + return $data;
1087 + }
1088 +
1089 + /**
1090 + * Apply theme defaults to empty fields for the live builder Preview, so the
1091 + * preview matches the design even before anything is saved.
1092 + * Hooked automatically via nx_preview_settings_{$this->id} (see Extension::init()).
1093 + *
1094 + * @param array $settings Flat preview settings.
1095 + * @return array
1096 + */
1097 + public function preview_settings( $settings ) {
1098 + $theme_full = ! empty( $settings['themes'] ) ? $settings['themes'] : ( ! empty( $settings['theme'] ) ? $settings['theme'] : '' );
1099 + return $this->fill_theme_defaults( $settings, $theme_full );
1100 + }
1101 +
1102 + /**
830 1103 * Generate entry key
831 1104 *
832 1105 * @param string $key
833 1106 * @return string
@@ -910,13 +1183,13 @@
910 1183 * @return string
911 1184 */
912 1185 private function get_user_ip() {
913 1186 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
914 - return $_SERVER['HTTP_CLIENT_IP'];
1187 + return sanitize_text_field(wp_unslash($_SERVER['HTTP_CLIENT_IP']));
915 1188 } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
916 - return $_SERVER['HTTP_X_FORWARDED_FOR'];
1189 + return sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']));
917 1190 } else {
918 - return $_SERVER['REMOTE_ADDR'] ?? '';
1191 + return isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
919 1192 }
920 1193 }
921 1194
922 1195
@@ -1042,8 +1315,9 @@
1042 1315 return $fields;
1043 1316 }
1044 1317
1045 1318 public function doc(){
1319 + /* translators: %1$s: step-by-step guides link URL, %2$s: tutorial link URL, %3$s: URL */
1046 1320 return sprintf(__('<p>Create compelling Announcements that capture visitor interest and help you generate more leads on your WordPress site. Need help? Check out our <a href="%1$s" target="_blank">step-by-step guides</a> to build impactful Announcements.</p>
1047 1321 <p>🎦 Watch the quick video <a target="_blank" href="%2$s">tutorial</a> for an easy walk-through.</p>
1048 1322 <p><strong>Recommended Blogs:</strong></p>
1049 1323 <p>🔥 <a target="_blank" href="%3$s">How to Create High-Performing Announcements with NotificationX?</a></p>