PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.5.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.5.0
5.6.1 5.6.0 5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 3.0.60 3.0.61 3.0.62 3.0.70 3.0.71 All 36 releases
double-opt-in / core / assets / doi-error-notification.js

doi-error-notification.js in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.5.0, at core/assets/doi-error-notification.js

265 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Double Opt-In — Universal Error Notification
3 *
4 * Listens for form submission events from any supported form plugin
5 * and checks the server-side AJAX endpoint for OptIn creation errors.
6 * Displays a toast notification if an error is found.
7 *
8 * @since 4.2.0
9 */
10 (function () {
11 'use strict';
12
13 if ( typeof doiErrorNotification === 'undefined' ) {
14 return;
15 }
16
17 var config = doiErrorNotification;
18 var checking = false;
19 var cooldown = false;
20
21 /**
22 * Check the AJAX endpoint for a stored submission error.
23 */
24 function checkForError() {
25 if ( checking || cooldown ) {
26 return;
27 }
28 checking = true;
29
30 var xhr = new XMLHttpRequest();
31 xhr._doiInternal = true; // Prevent the XHR interceptor from re-triggering scheduleCheck
32 xhr.open( 'POST', config.ajaxUrl, true );
33 xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
34
35 xhr.onreadystatechange = function () {
36 if ( xhr.readyState !== 4 ) {
37 return;
38 }
39 checking = false;
40
41 if ( xhr.status !== 200 ) {
42 return;
43 }
44
45 try {
46 var response = JSON.parse( xhr.responseText );
47 } catch ( e ) {
48 return;
49 }
50
51 if ( response.success && response.data && response.data.error ) {
52 // Hide form confirmation messages when validation error should be visible
53 if ( response.data.error.hide_confirmation ) {
54 hideFormConfirmations();
55 }
56
57 // Redirect to error page if configured, otherwise show toast
58 if ( response.data.redirect_url ) {
59 window.location.href = response.data.redirect_url;
60 return;
61 }
62
63 showNotification( response.data.error.message, 'error' );
64
65 // Prevent duplicate checks for 5 seconds
66 cooldown = true;
67 setTimeout( function () {
68 cooldown = false;
69 }, 5000 );
70 } else if ( response.success && response.data && response.data.success_message ) {
71 // A "sent — please confirm" message stashed server-side by an
72 // integration that die()s early (Avada) and so can't show the
73 // form plugin's own confirmation. Render it as a success toast.
74 showNotification( response.data.success_message, 'success' );
75
76 cooldown = true;
77 setTimeout( function () {
78 cooldown = false;
79 }, 5000 );
80 }
81 };
82
83 xhr.send(
84 'action=doi_check_submission_error&nonce=' + encodeURIComponent( config.nonce )
85 );
86 }
87
88 /**
89 * Schedule an error check with a short delay to ensure
90 * the server-side form processing has completed.
91 */
92 function scheduleCheck() {
93 setTimeout( checkForError, 800 );
94 }
95
96 /**
97 * Hide form plugin confirmation/success messages.
98 *
99 * Called when a validation error should be visible (block/redirect mode)
100 * so the user does not see a contradictory success message.
101 */
102 function hideFormConfirmations() {
103 // WPForms confirmation containers
104 var wpfSelectors = '.wpforms-confirmation-container-full, .wpforms-confirmation-container';
105 document.querySelectorAll( wpfSelectors ).forEach( function ( el ) {
106 el.style.display = 'none';
107 } );
108
109 // Gravity Forms confirmation wrapper
110 document.querySelectorAll( '.gform_confirmation_wrapper' ).forEach( function ( el ) {
111 el.style.display = 'none';
112 } );
113
114 // CF7 success response (only hide the success state, not error state)
115 document.querySelectorAll( '.wpcf7-response-output.wpcf7-mail-sent-ok' ).forEach( function ( el ) {
116 el.style.display = 'none';
117 } );
118
119 // Elementor Forms success message
120 document.querySelectorAll( '.elementor-message.elementor-message-success' ).forEach( function ( el ) {
121 el.style.display = 'none';
122 } );
123 }
124
125 /**
126 * Show a toast notification.
127 *
128 * @param {string} message The message to display.
129 * @param {string} [type] 'error' (default) or 'success' — controls colour + icon.
130 */
131 function showNotification( message, type ) {
132 type = type === 'success' ? 'success' : 'error';
133
134 var existing = document.querySelector( '.doi-error-notification' );
135 if ( existing ) {
136 existing.remove();
137 }
138
139 var notification = document.createElement( 'div' );
140 notification.className = 'doi-error-notification';
141 if ( type === 'success' ) {
142 notification.className += ' doi-error-notification--success';
143 }
144 notification.setAttribute( 'role', type === 'success' ? 'status' : 'alert' );
145
146 var content = document.createElement( 'div' );
147 content.className = 'doi-error-notification__content';
148
149 var icon = document.createElement( 'span' );
150 icon.className = 'doi-error-notification__icon';
151 // Check mark for success, warning triangle for error.
152 icon.innerHTML = type === 'success' ? '✓' : '⚠';
153
154 var text = document.createElement( 'p' );
155 text.className = 'doi-error-notification__message';
156 text.textContent = message;
157
158 var closeBtn = document.createElement( 'button' );
159 closeBtn.className = 'doi-error-notification__close';
160 closeBtn.setAttribute( 'type', 'button' );
161 closeBtn.innerHTML = '×';
162 closeBtn.addEventListener( 'click', function () {
163 dismiss( notification );
164 } );
165
166 content.appendChild( icon );
167 content.appendChild( text );
168 content.appendChild( closeBtn );
169 notification.appendChild( content );
170 document.body.appendChild( notification );
171
172 // Trigger enter animation
173 requestAnimationFrame( function () {
174 notification.classList.add( 'doi-error-notification--visible' );
175 } );
176
177 // Auto-dismiss after 10 seconds
178 setTimeout( function () {
179 dismiss( notification );
180 }, 10000 );
181 }
182
183 /**
184 * Dismiss a notification with exit animation.
185 *
186 * @param {HTMLElement} el The notification element.
187 */
188 function dismiss( el ) {
189 if ( ! el || ! el.parentNode ) {
190 return;
191 }
192 el.classList.remove( 'doi-error-notification--visible' );
193 el.classList.add( 'doi-error-notification--exit' );
194 setTimeout( function () {
195 if ( el.parentNode ) {
196 el.parentNode.removeChild( el );
197 }
198 }, 300 );
199 }
200
201 // -------------------------------------------------------------------------
202 // Form plugin event listeners
203 // -------------------------------------------------------------------------
204
205 // Contact Form 7 (native DOM events)
206 document.addEventListener( 'wpcf7mailsent', scheduleCheck );
207 document.addEventListener( 'wpcf7invalid', scheduleCheck );
208 document.addEventListener( 'wpcf7spam', scheduleCheck );
209 document.addEventListener( 'wpcf7mailfailed', scheduleCheck );
210
211 // WPForms + Gravity Forms (jQuery events — not caught by native addEventListener)
212 if ( typeof jQuery !== 'undefined' ) {
213 jQuery( document ).on( 'wpformsAjaxSubmitSuccess', scheduleCheck );
214 jQuery( document ).on( 'wpformsAjaxSubmitError', scheduleCheck );
215 jQuery( document ).on( 'gform_confirmation_loaded', scheduleCheck );
216 }
217
218 // -------------------------------------------------------------------------
219 // Universal: catch ANY form submission on the page (capture phase)
220 // This is the most reliable way to detect form submissions regardless
221 // of which form plugin is used. It covers AJAX, standard, and custom flows.
222 // -------------------------------------------------------------------------
223 document.addEventListener( 'submit', function () {
224 // Use a longer delay for standard (non-AJAX) submissions
225 // because the server needs time to process the request.
226 setTimeout( checkForError, 1500 );
227 }, true );
228
229 // -------------------------------------------------------------------------
230 // Generic AJAX interception
231 // Catches all AJAX calls to detect form submissions from plugins
232 // that don't fire specific events (e.g., Avada, Elementor).
233 // -------------------------------------------------------------------------
234
235 // Intercept fetch()
236 if ( typeof window.fetch === 'function' ) {
237 var originalFetch = window.fetch;
238 window.fetch = function () {
239 // Capture URL before the async call — `arguments` inside .then()
240 // refers to the callback's own arguments, not the fetch arguments.
241 var fetchUrl = ( typeof arguments[0] === 'string' )
242 ? arguments[0]
243 : ( arguments[0] && arguments[0].url ) || '';
244
245 return originalFetch.apply( this, arguments ).then( function ( response ) {
246 if ( fetchUrl && fetchUrl.indexOf( 'admin-ajax.php' ) !== -1 ) {
247 scheduleCheck();
248 }
249 return response;
250 } );
251 };
252 }
253
254 // Intercept XMLHttpRequest for jQuery.ajax based form plugins
255 var origOpen = XMLHttpRequest.prototype.open;
256 XMLHttpRequest.prototype.open = function ( method, url ) {
257 // Skip our own internal error-check requests to prevent infinite polling loops
258 if ( typeof url === 'string' && url.indexOf( 'admin-ajax.php' ) !== -1 && ! this._doiInternal ) {
259 this.addEventListener( 'load', scheduleCheck );
260 }
261 return origOpen.apply( this, arguments );
262 };
263
264 })();
265