PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.6.2
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.6.2
5.6.2 5.6.3 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 All 38 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.6.2, at core/assets/doi-error-notification.js

273 lines 8.7 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 // A toast that stands in for a hidden success message stays until
64 // the visitor closes it — otherwise the form is left saying
65 // nothing at all after ten seconds.
66 showNotification( response.data.error.message, 'error', !! response.data.error.hide_confirmation );
67
68 // Prevent duplicate checks for 5 seconds
69 cooldown = true;
70 setTimeout( function () {
71 cooldown = false;
72 }, 5000 );
73 } else if ( response.success && response.data && response.data.success_message ) {
74 // A "sent — please confirm" message stashed server-side by an
75 // integration that die()s early (Avada) and so can't show the
76 // form plugin's own confirmation. Render it as a success toast.
77 showNotification( response.data.success_message, 'success' );
78
79 cooldown = true;
80 setTimeout( function () {
81 cooldown = false;
82 }, 5000 );
83 }
84 };
85
86 xhr.send(
87 'action=doi_check_submission_error&nonce=' + encodeURIComponent( config.nonce )
88 );
89 }
90
91 /**
92 * Schedule an error check with a short delay to ensure
93 * the server-side form processing has completed.
94 */
95 function scheduleCheck() {
96 setTimeout( checkForError, 800 );
97 }
98
99 /**
100 * Hide form plugin confirmation/success messages.
101 *
102 * Called when a validation error should be visible (block/redirect mode)
103 * so the user does not see a contradictory success message.
104 */
105 function hideFormConfirmations() {
106 // WPForms confirmation containers
107 var wpfSelectors = '.wpforms-confirmation-container-full, .wpforms-confirmation-container';
108 document.querySelectorAll( wpfSelectors ).forEach( function ( el ) {
109 el.style.display = 'none';
110 } );
111
112 // Gravity Forms confirmation wrapper
113 document.querySelectorAll( '.gform_confirmation_wrapper' ).forEach( function ( el ) {
114 el.style.display = 'none';
115 } );
116
117 // CF7 success response (only hide the success state, not error state)
118 document.querySelectorAll( '.wpcf7-response-output.wpcf7-mail-sent-ok' ).forEach( function ( el ) {
119 el.style.display = 'none';
120 } );
121
122 // Elementor Forms success message
123 document.querySelectorAll( '.elementor-message.elementor-message-success' ).forEach( function ( el ) {
124 el.style.display = 'none';
125 } );
126 }
127
128 /**
129 * Show a toast notification.
130 *
131 * @param {string} message The message to display.
132 * @param {string} [type] 'error' (default) or 'success' — controls colour + icon.
133 * @param {boolean} [sticky] Keep it until the visitor closes it.
134 */
135 function showNotification( message, type, sticky ) {
136 type = type === 'success' ? 'success' : 'error';
137
138 var existing = document.querySelector( '.doi-error-notification' );
139 if ( existing ) {
140 existing.remove();
141 }
142
143 var notification = document.createElement( 'div' );
144 notification.className = 'doi-error-notification';
145 if ( type === 'success' ) {
146 notification.className += ' doi-error-notification--success';
147 }
148 notification.setAttribute( 'role', type === 'success' ? 'status' : 'alert' );
149
150 var content = document.createElement( 'div' );
151 content.className = 'doi-error-notification__content';
152
153 var icon = document.createElement( 'span' );
154 icon.className = 'doi-error-notification__icon';
155 // Check mark for success, warning triangle for error.
156 icon.innerHTML = type === 'success' ? '✓' : '⚠';
157
158 var text = document.createElement( 'p' );
159 text.className = 'doi-error-notification__message';
160 text.textContent = message;
161
162 var closeBtn = document.createElement( 'button' );
163 closeBtn.className = 'doi-error-notification__close';
164 closeBtn.setAttribute( 'type', 'button' );
165 closeBtn.innerHTML = '×';
166 closeBtn.addEventListener( 'click', function () {
167 dismiss( notification );
168 } );
169
170 content.appendChild( icon );
171 content.appendChild( text );
172 content.appendChild( closeBtn );
173 notification.appendChild( content );
174 document.body.appendChild( notification );
175
176 // Trigger enter animation
177 requestAnimationFrame( function () {
178 notification.classList.add( 'doi-error-notification--visible' );
179 } );
180
181 if ( sticky ) {
182 return;
183 }
184
185 // Auto-dismiss after 10 seconds
186 setTimeout( function () {
187 dismiss( notification );
188 }, 10000 );
189 }
190
191 /**
192 * Dismiss a notification with exit animation.
193 *
194 * @param {HTMLElement} el The notification element.
195 */
196 function dismiss( el ) {
197 if ( ! el || ! el.parentNode ) {
198 return;
199 }
200 el.classList.remove( 'doi-error-notification--visible' );
201 el.classList.add( 'doi-error-notification--exit' );
202 setTimeout( function () {
203 if ( el.parentNode ) {
204 el.parentNode.removeChild( el );
205 }
206 }, 300 );
207 }
208
209 // -------------------------------------------------------------------------
210 // Form plugin event listeners
211 // -------------------------------------------------------------------------
212
213 // Contact Form 7 (native DOM events)
214 document.addEventListener( 'wpcf7mailsent', scheduleCheck );
215 document.addEventListener( 'wpcf7invalid', scheduleCheck );
216 document.addEventListener( 'wpcf7spam', scheduleCheck );
217 document.addEventListener( 'wpcf7mailfailed', scheduleCheck );
218
219 // WPForms + Gravity Forms (jQuery events — not caught by native addEventListener)
220 if ( typeof jQuery !== 'undefined' ) {
221 jQuery( document ).on( 'wpformsAjaxSubmitSuccess', scheduleCheck );
222 jQuery( document ).on( 'wpformsAjaxSubmitError', scheduleCheck );
223 jQuery( document ).on( 'gform_confirmation_loaded', scheduleCheck );
224 }
225
226 // -------------------------------------------------------------------------
227 // Universal: catch ANY form submission on the page (capture phase)
228 // This is the most reliable way to detect form submissions regardless
229 // of which form plugin is used. It covers AJAX, standard, and custom flows.
230 // -------------------------------------------------------------------------
231 document.addEventListener( 'submit', function () {
232 // Use a longer delay for standard (non-AJAX) submissions
233 // because the server needs time to process the request.
234 setTimeout( checkForError, 1500 );
235 }, true );
236
237 // -------------------------------------------------------------------------
238 // Generic AJAX interception
239 // Catches all AJAX calls to detect form submissions from plugins
240 // that don't fire specific events (e.g., Avada, Elementor).
241 // -------------------------------------------------------------------------
242
243 // Intercept fetch()
244 if ( typeof window.fetch === 'function' ) {
245 var originalFetch = window.fetch;
246 window.fetch = function () {
247 // Capture URL before the async call — `arguments` inside .then()
248 // refers to the callback's own arguments, not the fetch arguments.
249 var fetchUrl = ( typeof arguments[0] === 'string' )
250 ? arguments[0]
251 : ( arguments[0] && arguments[0].url ) || '';
252
253 return originalFetch.apply( this, arguments ).then( function ( response ) {
254 if ( fetchUrl && fetchUrl.indexOf( 'admin-ajax.php' ) !== -1 ) {
255 scheduleCheck();
256 }
257 return response;
258 } );
259 };
260 }
261
262 // Intercept XMLHttpRequest for jQuery.ajax based form plugins
263 var origOpen = XMLHttpRequest.prototype.open;
264 XMLHttpRequest.prototype.open = function ( method, url ) {
265 // Skip our own internal error-check requests to prevent infinite polling loops
266 if ( typeof url === 'string' && url.indexOf( 'admin-ajax.php' ) !== -1 && ! this._doiInternal ) {
267 this.addEventListener( 'load', scheduleCheck );
268 }
269 return origOpen.apply( this, arguments );
270 };
271
272 })();
273