PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 4.0.0
WP 2FA – Two-factor authentication for WordPress v4.0.0
4.0.0 1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / includes / assets / js / wizard-new.js
wp-2fa / includes / assets / js Last commit date
customize-styles.js 2 days ago exclude-self-prompt.js 2 days ago intlTelInput-vanilla.js 2 days ago policies-hash-state.js 2 days ago save-policies-new.js 2 days ago save-settings-new.js 2 days ago settings-design-logic.js 2 days ago settings-hash-state.js 2 days ago test-email-new.js 2 days ago wizard-new.js 2 days ago
wizard-new.js
410 lines
1 /**
2 * WP 2FA – New First-Time Setup Wizard
3 *
4 * Three-screen wizard: Welcome → Steps (with nav) → Finish.
5 * Vanilla JS, no jQuery, no transpiler.
6 *
7 * @package wp-2fa
8 * @since 3.2.0
9 */
10
11 ( function () {
12 'use strict';
13
14 function boot() {
15
16 var cfg = window.wp2faWizardNew || {};
17
18 /* ── Screen references ────────────────────── */
19
20 var welcomeScreen = document.getElementById( 'wp2fa-wizard-welcome' );
21 var stepsScreen = document.getElementById( 'wp2fa-wizard-steps-screen' );
22 var finishScreen = document.getElementById( 'wp2fa-wizard-finish' );
23
24 if ( ! welcomeScreen || ! stepsScreen || ! finishScreen ) {
25 return;
26 }
27
28 /* ── Step panels ──────────────────────────── */
29
30 var panels = Array.prototype.slice.call( stepsScreen.querySelectorAll( '.wp2fa-wizard-panel' ) );
31 var navItems = Array.prototype.slice.call( stepsScreen.querySelectorAll( '.wp2fa-wizard-nav li' ) );
32 var continueBtn = stepsScreen.querySelector( '.js-wizard-continue' );
33 var finishBtn = stepsScreen.querySelector( '.js-wizard-finish' );
34 var form = document.getElementById( 'wp2fa-wizard-form' );
35 var currentStep = 0;
36
37 /* ── Screen switching ─────────────────────── */
38
39 function showScreen( screen ) {
40 welcomeScreen.style.display = 'none';
41 welcomeScreen.classList.remove( 'wp2fa-wizard-screen--active' );
42 stepsScreen.style.display = 'none';
43 finishScreen.style.display = 'none';
44
45 screen.style.display = '';
46 screen.classList.add( 'wp2fa-wizard-screen--active' );
47 window.scrollTo( 0, 0 );
48 }
49
50 function setFinishMode( isCurrentUserExcluded ) {
51 var excludedBlock = finishScreen.querySelector( '[data-finish-excluded]' );
52 var normalBlock = finishScreen.querySelector( '[data-finish-normal]' );
53
54 if ( ! excludedBlock || ! normalBlock ) {
55 return;
56 }
57
58 excludedBlock.style.display = isCurrentUserExcluded ? '' : 'none';
59 normalBlock.style.display = isCurrentUserExcluded ? 'none' : '';
60 }
61
62 /* ── Step navigation ──────────────────────── */
63
64 function showPanel( idx ) {
65 panels.forEach( function ( panel, i ) {
66 panel.style.display = ( i === idx ) ? '' : 'none';
67 } );
68 currentStep = idx;
69 updateNav();
70 updateButtons();
71 window.scrollTo( 0, 0 );
72 }
73
74 function updateNav() {
75 navItems.forEach( function ( li, i ) {
76 li.classList.remove( 'is-active', 'is-done' );
77 if ( i === currentStep ) {
78 li.classList.add( 'is-active' );
79 } else if ( i < currentStep ) {
80 li.classList.add( 'is-done' );
81 }
82 } );
83 }
84
85 function updateButtons() {
86 var isLast = ( currentStep === panels.length - 1 );
87 if ( continueBtn ) {
88 continueBtn.style.display = isLast ? 'none' : '';
89 }
90 if ( finishBtn ) {
91 finishBtn.style.display = isLast ? '' : 'none';
92 }
93 validateMethodsPanel();
94 }
95
96 /* ── Methods-panel validation ─────────────── */
97
98 var methodsPanel = stepsScreen.querySelector( '[data-panel="methods"]' );
99
100 function hasCheckedMethod() {
101 if ( ! methodsPanel ) {
102 return true;
103 }
104 var checkboxes = methodsPanel.querySelectorAll( '.wp2fa-sortable-checkbox input[type="checkbox"], .wizard-method-card input[type="checkbox"]' );
105 for ( var i = 0; i < checkboxes.length; i++ ) {
106 if ( checkboxes[ i ].checked ) {
107 return true;
108 }
109 }
110 return false;
111 }
112
113 function validateMethodsPanel() {
114 if ( currentStep !== 0 || ! methodsPanel ) {
115 return;
116 }
117 var valid = hasCheckedMethod();
118 var notice = methodsPanel.querySelector( '.wp2fa-wizard-methods-notice' );
119
120 if ( ! valid ) {
121 if ( continueBtn ) {
122 continueBtn.disabled = true;
123 continueBtn.classList.add( 'disabled' );
124 }
125 if ( ! notice ) {
126 notice = document.createElement( 'div' );
127 notice.className = 'wp2fa-wizard-methods-notice';
128 notice.textContent = cfg.methodsRequiredText || 'Please select at least one 2FA method';
129 var desc = methodsPanel.querySelector( 'p.description' );
130 if ( desc ) {
131 desc.parentNode.insertBefore( notice, desc.nextSibling );
132 } else {
133 methodsPanel.insertBefore( notice, methodsPanel.firstChild );
134 }
135 }
136 } else {
137 if ( continueBtn ) {
138 continueBtn.disabled = false;
139 continueBtn.classList.remove( 'disabled' );
140 }
141 if ( notice ) {
142 notice.remove();
143 }
144 }
145 }
146
147 // Listen for checkbox changes inside the methods panel.
148 if ( methodsPanel ) {
149 methodsPanel.addEventListener( 'change', function ( e ) {
150 if ( e.target.matches( '.wp2fa-sortable-checkbox input[type="checkbox"], .wizard-method-card input[type="checkbox"]' ) ) {
151 validateMethodsPanel();
152 }
153 } );
154 }
155
156 function nextPanel() {
157 if ( currentStep < panels.length - 1 ) {
158 showPanel( currentStep + 1 );
159 }
160 }
161
162 /* ── Collect form fields ──────────────────── */
163
164 function collectFields() {
165 if ( ! form ) {
166 return {};
167 }
168 var data = {};
169 var elements = form.querySelectorAll( 'input, select, textarea' );
170
171 elements.forEach( function ( el ) {
172 if ( ! el.name || el.disabled ) {
173 return;
174 }
175 if ( el.type === 'submit' || el.type === 'button' || el.type === 'image' ) {
176 return;
177 }
178
179 var name = el.name;
180 var value;
181
182 if ( el.type === 'checkbox' ) {
183 if ( el.checked ) {
184 value = el.value !== '' ? el.value : '1';
185 } else {
186 if ( /\[\]$/.test( name ) ) {
187 return;
188 }
189 value = '';
190 }
191 } else if ( el.type === 'radio' ) {
192 if ( ! el.checked ) {
193 return;
194 }
195 value = el.value;
196 } else {
197 value = el.value;
198 }
199
200 if ( Object.prototype.hasOwnProperty.call( data, name ) ) {
201 if ( ! Array.isArray( data[ name ] ) ) {
202 data[ name ] = [ data[ name ] ];
203 }
204 data[ name ].push( value );
205 } else {
206 data[ name ] = value;
207 }
208 } );
209
210 return data;
211 }
212
213 function serialise( obj ) {
214 var parts = [];
215 for ( var key in obj ) {
216 if ( ! Object.prototype.hasOwnProperty.call( obj, key ) ) {
217 continue;
218 }
219 var val = obj[ key ];
220 if ( Array.isArray( val ) ) {
221 val.forEach( function ( v ) {
222 parts.push( encodeURIComponent( key ) + '=' + encodeURIComponent( v ) );
223 } );
224 } else {
225 parts.push( encodeURIComponent( key ) + '=' + encodeURIComponent( val ) );
226 }
227 }
228 return parts.join( '&' );
229 }
230
231 /* ── AJAX save ────────────────────────────── */
232
233 function saveWizard( btn ) {
234 var origText = btn.textContent;
235 btn.disabled = true;
236 btn.textContent = cfg.savingText || 'Saving…';
237
238 var fields = collectFields();
239 var body = serialise( fields );
240 body += '&action=' + encodeURIComponent( cfg.action || 'wp2fa_wizard_save' );
241 body += '&nonce=' + encodeURIComponent( cfg.nonce || '' );
242
243 var xhr = new XMLHttpRequest();
244 xhr.open( 'POST', cfg.ajaxUrl || window.ajaxurl, true );
245 xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' );
246
247 xhr.onload = function () {
248 btn.disabled = false;
249 btn.textContent = origText;
250
251 if ( xhr.status >= 200 && xhr.status < 300 ) {
252 try {
253 var resp = JSON.parse( xhr.responseText );
254 if ( resp.success ) {
255 if ( resp.data && typeof resp.data.isCurrentUserExcluded !== 'undefined' ) {
256 setFinishMode( !! resp.data.isCurrentUserExcluded );
257 }
258 showScreen( finishScreen );
259 return;
260 }
261 if ( resp.data && resp.data.message ) {
262 showError( btn, resp.data.message );
263 return;
264 }
265 } catch ( e ) { /* fall through */ }
266 }
267 showError( btn, cfg.errorText || 'An error occurred. Please try again.' );
268 };
269
270 xhr.onerror = function () {
271 btn.disabled = false;
272 btn.textContent = origText;
273 showError( btn, cfg.errorText || 'An error occurred. Please try again.' );
274 };
275
276 xhr.send( body );
277 }
278
279 function showError( btn, message ) {
280 var parent = btn.parentNode;
281 var existing = parent.querySelector( '.wp2fa-wizard-error' );
282 if ( existing ) {
283 existing.remove();
284 }
285
286 var notice = document.createElement( 'span' );
287 notice.className = 'wp2fa-wizard-error';
288 notice.textContent = message;
289 parent.insertBefore( notice, btn.nextSibling );
290
291 setTimeout( function () {
292 if ( notice.parentNode ) {
293 notice.remove();
294 }
295 }, 5000 );
296 }
297
298 /* ── Toggle sub-fields (grace period, etc.) ── */
299
300 function initToggles() {
301 var radios = document.querySelectorAll( '[data-toggle-target]' );
302 radios.forEach( function ( radio ) {
303 var targetSel = radio.getAttribute( 'data-toggle-target' );
304 if ( ! targetSel ) {
305 return;
306 }
307
308 radio.addEventListener( 'change', function () {
309 var target = document.querySelector( targetSel );
310 if ( target ) {
311 target.style.display = radio.checked ? '' : 'none';
312 }
313 } );
314
315 // Hide when a sibling radio (same name) is selected.
316 var siblings = document.querySelectorAll( 'input[type="radio"][name="' + radio.name + '"]' );
317 siblings.forEach( function ( sib ) {
318 if ( sib === radio ) {
319 return;
320 }
321 sib.addEventListener( 'change', function () {
322 var target = document.querySelector( targetSel );
323 if ( target && sib.checked ) {
324 target.style.display = 'none';
325 }
326 } );
327 } );
328 } );
329 }
330
331 /* ── Multi-select AJAX search wiring ────────── */
332
333 function initMultiSelectSearch() {
334 if ( ! window.wp2faSavePoliciesNew ) {
335 window.wp2faSavePoliciesNew = {
336 ajaxUrl: cfg.ajaxUrl,
337 nonce: cfg.searchNonce || cfg.nonce,
338 searchAction: cfg.searchAction || 'wp2fa_search_policy_items',
339 };
340 }
341 }
342
343 /* ── Event delegation ─────────────────────── */
344
345 // Welcome screen: "Let's get started!" button.
346 welcomeScreen.addEventListener( 'click', function ( e ) {
347 if ( e.target.closest( '.js-wizard-start' ) ) {
348 showScreen( stepsScreen );
349 showPanel( 0 );
350 }
351 } );
352
353 // Steps screen: Continue and Finish buttons.
354 stepsScreen.addEventListener( 'click', function ( e ) {
355 if ( e.target.closest( '.js-wizard-continue' ) ) {
356 if ( continueBtn && continueBtn.disabled ) {
357 return;
358 }
359 nextPanel();
360 return;
361 }
362 if ( e.target.closest( '.js-wizard-finish' ) ) {
363 saveWizard( e.target.closest( '.js-wizard-finish' ) );
364 }
365 } );
366
367 /* ── Skip wizard confirmation ─────────────── */
368
369 document.addEventListener( 'click', function ( e ) {
370 var skipLink = e.target.closest( '.wp2fa-wizard-skip-link' );
371 if ( ! skipLink ) {
372 return;
373 }
374
375 // Don't show confirmation on the finish screen.
376 if ( skipLink.closest( '#wp2fa-wizard-finish' ) ) {
377 return;
378 }
379
380 e.preventDefault();
381
382 var targetUrl = skipLink.getAttribute( 'href' );
383
384 wp2faDialog.confirm( {
385 title: '',
386 message: cfg.skipConfirmMessage || '',
387 confirmText: cfg.skipConfirmOk || 'OK',
388 cancelText: cfg.skipConfirmCancel || 'Cancel',
389 onConfirm: function () {
390 window.location.href = targetUrl;
391 }
392 } );
393 } );
394
395 /* ── Initialise ──────────────────────────── */
396
397 initToggles();
398 initMultiSelectSearch();
399 updateButtons();
400
401 } // end boot()
402
403 if ( document.readyState === 'loading' ) {
404 document.addEventListener( 'DOMContentLoaded', boot );
405 } else {
406 boot();
407 }
408
409 } )();
410