1/** 2 * CherryRoad Paywall â Frontend Auth JS 3 * Handles: login-only modal (Modal 1), unified account modal (Modals 2+3), logout, password toggle. 4 */ 5(function () { 6 'use strict'; 7 8 const restBase = crpwData.restUrl; 9 const nonce = crpwData.nonce; 10 const homeUrl = crpwData.homeUrl; 11 12 // ---------------------------------------------------------------- 13 // Content URL tracking â set crpw_content_url cookie client-side. 14 // Done in JS (not PHP) so it works correctly on cached pages: cached 15 // pages skip PHP so a server-side cookie would never be set, but JS 16 // always runs in the browser. Stores the current article URL so the 17 // login / signup / checkout flows can redirect the visitor back here. 18 // Only set on singular content where this script loads, and never on 19 // the plugin's own flow pages (handled by not enqueuing there / guard below). 20 // ---------------------------------------------------------------- 21 (function setContentUrlCookie() { 22 try { 23 // Don't overwrite on our own flow pages â match query/`crpw` markers 24 const path = window.location.pathname.toLowerCase(); 25 const skip = ['/plans', '/checkout', '/dashboard', '/login', '/crpw-login', '/subscribe', '/reset', '/continue']; 26 for (const s of skip) { 27 if (path === s || path === s + '/' || path.indexOf(s + '/') === 0) return; 28 } 29 // Strip our own cache-bust param so the stored URL is clean 30 const url = new URL(window.location.href); 31 url.searchParams.delete('crpw_auth'); 32 const clean = url.toString(); 33 34 const secure = window.location.protocol === 'https:' ? '; Secure' : ''; 35 const maxAge = 60 * 60; // 1 hour, mirrors previous PHP cookie lifetime 36 document.cookie = 'crpw_content_url=' + encodeURIComponent(clean) + 37 '; path=/; max-age=' + maxAge + '; SameSite=Lax' + secure; 38 } catch (e) { /* non-fatal â return-to-article is a convenience only */ } 39 })(); 40 41 // Append ?crpw_auth=1 to redirect URLs after login â tells WP Rocket to bypass cache 42 // for that first page load. WP Rocket must have crpw_auth in its query string exclusions. 43 function cacheBustUrl( url ) { 44 if ( ! url ) return url; 45 const sep = url.includes('?') ? '&' : '?'; 46 return url + sep + 'crpw_auth=1'; 47 } 48 49 const apiPost = (endpoint, body) => 50 fetch(restBase + endpoint, { 51 method: 'POST', 52 headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-WP-Nonce': nonce }, 53 body: new URLSearchParams(body).toString(), 54 }).then(r => r.json()); 55 56 // ================================================================ 57 // Modal 1 â Login only (paywall gate Log In button, checkout page) 58 // ================================================================ 59 const loginOverlay = document.getElementById('crpw-modal-overlay'); 60 const loginOpenBtn = document.getElementById('crpw-open-modal'); 61 const loginCloseBtn = document.getElementById('crpw-modal-close'); 62 63 // Move to document.body â escapes any theme stacking context so 64 // position:fixed works correctly regardless of scroll position 65 if (loginOverlay) document.body.appendChild(loginOverlay); 66 67 function openLoginModal() { 68 if (!loginOverlay) return; 69 loginOverlay.removeAttribute('hidden'); 70 requestAnimationFrame(() => requestAnimationFrame(() => 71 loginOverlay.classList.add('crpw-modal-visible') 72 )); 73 document.body.style.overflow = 'hidden'; 74 const first = loginOverlay.querySelector('input:not([type=hidden])'); 75 if (first) setTimeout(() => first.focus(), 250); 76 } 77 78 function closeLoginModal() { 79 if (!loginOverlay) return; 80 loginOverlay.classList.remove('crpw-modal-visible'); 81 document.body.style.overflow = ''; 82 setTimeout(() => { loginOverlay.setAttribute('hidden', ''); if (loginOpenBtn) loginOpenBtn.focus(); }, 220); 83 } 84 85 if (loginOpenBtn) loginOpenBtn.addEventListener('click', openLoginModal); 86 if (loginCloseBtn) loginCloseBtn.addEventListener('click', closeLoginModal); 87 if (loginOverlay) loginOverlay.addEventListener('click', e => { if (e.target === loginOverlay) closeLoginModal(); }); 88 89 document.addEventListener('keydown', e => { 90 if (e.key === 'Escape' && loginOverlay && !loginOverlay.hasAttribute('hidden')) closeLoginModal(); 91 }); 92 93 // Focus trap for login modal 94 if (loginOverlay) { 95 loginOverlay.addEventListener('keydown', function (e) { 96 if (e.key !== 'Tab') return; 97 const els = loginOverlay.querySelectorAll('a[href],button:not([disabled]),input
97:not([disabled]),[tabindex]:not([tabindex="-1"])'); 98 const first = els[0], last = els[els.length - 1]; 99 if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last.focus(); } } 100 else { if (document.activeElement === last) { e.preventDefault(); first.focus(); } } 101 }); 102 } 103 104 // Login form handler (Modal 1) 105 const loginForm = document.getElementById('crpw-login-form'); 106 if (loginForm) { 107 const errorBox = document.getElementById('crpw-modal-error') || document.getElementById('crpw-login-error'); 108 const successBox = document.getElementById('crpw-modal-success') || document.getElementById('crpw-login-success'); 109 const submitBtn = document.getElementById('crpw-submit-btn'); 110 const btnText = submitBtn?.querySelector('.crpw-btn-text'); 111 const btnSpinner = submitBtn?.querySelector('.crpw-btn-spinner'); 112 113 loginForm.addEventListener('submit', async function (e) { 114 e.preventDefault(); 115 if (errorBox) { errorBox.style.display = 'none'; errorBox.textContent = ''; } 116 if (successBox) { successBox.style.display = 'none'; } 117 118 const username = loginForm.querySelector('[name="username"]')?.value.trim() ?? ''; 119 const password = loginForm.querySelector('[name="password"]')?.value ?? ''; 120 const redirectTo = loginForm.querySelector('[name="redirect_to"]')?.value || window.location.href; 121 122 if (!username || !password) { showLoginError('Please enter your email/username and password.'); return; } 123 124 setLoginLoading(true); 125 try { 126 const json = await apiPost('login', { username, password, redirect_to: redirectTo }); 127 if (json.status !== 'success') { 128 showLoginError(json.message || 'Login failed. Please check your credentials.'); 129 return; 130 } if (successBox) { successBox.textContent = 'â Login successful! Loadingâ¦'; successBox.style.display = 'block'; } 131 setTimeout(() => { window.location.href = cacheBustUrl( json.next_url || redirectTo || homeUrl ); }, 700); 132 } catch (err) { 133 showLoginError('A network error occurred. Please try again.'); 134 } finally { 135 setLoginLoading(false); 136 } 137 }); 138 139 function showLoginError(msg) { if (!errorBox) return; errorBox.textContent = msg; errorBox.style.display = 'block'; } 140 function setLoginLoading(on) { 141 if (!submitBtn) return; 142 submitBtn.disabled = on; 143 if (btnText) btnText.style.display = on ? 'none' : ''; 144 if (btnSpinner) btnSpinner.style.display = on ? 'inline-block' : 'none'; 145 } 146 } 147 148 // ================================================================ 149 // Unified Account Modal â Create Account + Login tabs 150 // Used from: paywall gate (Get Free Access), plans page (Subscribe + Free) 151 // Opened via: openAccountModal(config) called from crpw-plans.js and paywall gate buttons 152 // ================================================================ 153 const accountOverlay = document.getElementById('crpw-account-modal-overlay'); 154 const accountCloseBtn = document.getElementById('crpw-account-modal-close'); 155 156 // Move to document.body â escapes any theme stacking context 157 if (accountOverlay) document.body.appendChild(accountOverlay); 158 159 // Tab switcher â exposed globally so PHP onclick attributes work 160 window.crpwAccountSwitchTab = function(tab) { 161 const signupPane = document.getElementById('crpw-account-pane-signup'); 162 const loginPane = document.getElementById('crpw-account-pane-login'); 163 const signupTab = document.getElementById('crpw-account-tab-signup'); 164 const loginTab = document.getElementById('crpw-account-tab-login'); 165 if (!signupPane || !loginPane) return; 166 167 if (tab === 'signup') { 168 signupPane.style.display = ''; 169 loginPane.style.display = 'none'; 170 if (signupTab) signupTab.classList.add('crpw-modal-tab--active'); 171 if (loginTab) loginTab.classList.remove('crpw-modal-tab--active'); 172 } else { 173 signupPane.style.display = 'none'; 174 loginPane.style.display = ''; 175 if (loginTab) loginTab.classList.add('crpw-modal-tab--active'); 176 if (signupTab) signupTab.classList.remove('crpw-modal-tab--active'); 177 } 178 // Clear errors on tab switch 179 const errBox = document.getElementById('crpw-account-error'); 180 const loginErr = document.getElementById('crpw-account-login-error'); 181 if (errBox) { errBox.style.display = 'none'; errBox.textContent = ''; } 182 if (loginErr) { loginErr.style.display = 'none'; loginErr.textContent = ''; } 183 }; 184 185 // Open the unified account modal
186 // config: { mode: 'subscribe'|'free', planId, planName, planPrice, redirectTo } 187 window.openAccountModal = function(config) { 188 if (!accountOverlay) return; 189 190 const mode = config.mode || 'free'; 191 const planId = config.planId || ''; 192 const planName = config.planName || ''; 193 const planPrice = config.planPrice || ''; 194 const redirectTo = config.redirectTo || window.location.href; 195 196 // Set hidden fields 197 const planIdInput = document.getElementById('crpw-account-plan-id'); 198 const freeInput = document.getElementById('crpw-account-free'); 199 const signupRedirect = document.getElementById('crpw-account-signup-redirect'); 200 const loginRedirect = document.getElementById('crpw-account-login-redirect'); 201 202 if (planIdInput) planIdInput.value = planId; 203 if (freeInput) freeInput.value = mode === 'free' ? '1' : '0'; 204 if (signupRedirect) signupRedirect.value = redirectTo; 205 if (loginRedirect) loginRedirect.value = redirectTo; 206 207 // Plan summary bar â shown for paid subscribe 208 const planSummary = document.getElementById('crpw-account-plan-summary'); 209 if (planSummary) { 210 if (mode === 'subscribe' && planName) { 211 planSummary.textContent = 'Selected: ' + planName + ' â ' + planPrice; 212 planSummary.style.display = 'block'; 213 } else { 214 planSummary.style.display = 'none'; 215 } 216 } 217 218 // Modal title â shown for free mode 219 const modalTitle = document.getElementById('crpw-account-modal-title'); 220 if (modalTitle) modalTitle.style.display = mode === 'free' ? 'block' : 'none'; 221 222 // Submit button text 223 const submitText = document.querySelector('#crpw-account-signup-submit .crpw-btn-text'); 224 if (submitText) { 225 submitText.textContent = mode === 'free' ? 'Create Free Account' : 'Continue to Payment'; 226 } 227 228 // Footer links â shown in free mode only 229 const signupFooter = document.getElementById('crpw-account-plans-footer'); 230 const loginFooter = document.getElementById('crpw-account-login-footer'); 231 if (signupFooter) signupFooter.style.display = mode === 'free' ? 'block' : 'none'; 232 if (loginFooter) loginFooter.style.display = mode === 'free' ? 'block' : 'none'; 233 234 // Reset to signup tab, account step 235 crpwAccountSwitchTab('signup'); 236 accountShowStep('account'); 237 238 // Reset forms 239 document.getElementById('crpw-account-signup-form')?.reset(); 240 document.getElementById('crpw-account-login-form')?.reset(); 241 242 // Restore hidden field values after reset 243 if (planIdInput) planIdInput.value = planId; 244 if (freeInput) freeInput.value = mode === 'free' ? '1' : '0'; 245 if (signupRedirect) signupRedirect.value = redirectTo; 246 if (loginRedirect) loginRedirect.value = redirectTo; 247 248 // Reset password hint 249 const pwdHint = document.getElementById('crpw-account-password-hint'); 250 if (pwdHint) pwdHint.style.color = ''; 251 252 // Open 253 accountOverlay.removeAttribute('hidden'); 254 requestAnimationFrame(() => requestAnimationFrame(() => 255 accountOverlay.classList.add('crpw-modal-visible') 256 )); 257 document.body.style.overflow = 'hidden'; 258 setTimeout(() => { 259 const first = accountOverlay.querySelector('#crpw-account-pane-signup input:not([type=hidden])'); 260 if (first) first.focus(); 261 }, 250); 262 }; 263 264 function closeAccountModal() { 265 if (!accountOverlay) return; 266 accountOverlay.classList.remove('crpw-modal-visible'); 267 document.body.style.overflow = ''; 268 setTimeout(() => accountOverlay.setAttribute('hidden', ''), 220); 269 } 270 271 function accountShowStep(step) { 272 accountOverlay?.querySelectorAll('.crpw-subscribe-step').forEach(el => el.style.display = 'none'); 273 const target = document.getElementById('crpw-account-step-' + step); 274 if (target) target.style.display = 'block'; 275 } 276 277 if (accountCloseBtn) accountCloseBtn.addEventListener('click', closeAccountModal); 278 if (accountOverlay) accountOverlay.addEventListener('click', e => { if (e.target === accountOverlay) closeAccountModal(); }); 279 document.addEventListener('keydown', e => { 280 if (e.key === 'Escape' && accountOverlay && !accountOverlay.hasAttribute('hidden')) closeAccountModal(); 281 }); 282 283 // ---------------------------------------------------------------- 284 // Get Free Access button on paywall gate 285 // ---------------------------------------------------------------- 286 document.querySelectorAll('#crpw-open-free-modal').forEach(btn => { 287 btn.addEventListener('click', () => { 288 openAccountModal({ mode: 'free', redirectTo: window.location.href }); 289 }); 290 }); 291 292 // ---------------------------------------------------------------- 293 // Account modal â signup form 294 // ---------------------------------------------------------------- 295 const accountSignupForm = document.getElementById('crpw-account-signup-form'); 296 if (accountSignupForm) { 297 const errBox = document.getElementById('crpw-account-error'); 298 const submitBtn = document.getElementById('crpw-account-signup-submit'); 299 const btnText = submitBtn?.querySelector('.crpw-btn-text'); 300 const btnSpin = submitBtn?.querySelector('.crpw-btn-spinner'); 301 302 accountSignupForm.addEventListener('submit', async function(e) { 303 e.preventDefault(); 304 if (errBox) { errBox.style.display = 'none'; errBox.textContent = ''; } 305 306 // Client-side password validation 307 const pwd = accountSignupForm.querySelector('[name="password"]')?.value || ''; 308 const pwd2 = accountSignupForm.querySelector('[name="password2"]')?.value || ''; 309 const isFree = document.getElementById('crpw-account-free')?.value === '1'; 310 311 if (pwd !== pwd2) { 312 if (errBox) { errBox.textContent = 'Passwords do not match.'; errBox.style.display = 'block'; } 313 return; 314 } 315 if (pwd.length < 8 || !/[a-zA-Z]/.test(pwd) || !/[0-9!@#$%^&*()\-_=+\[\]{};':"\\|,.<>/?]/.test(pwd)) { 316 if (errBox) { errBox.textContent = 'Password must be at least 8 characters and contain a letter and a number or symbol.'; errBox.style.display = 'block'; } 317 return; 318 } 319 320 if (submitBtn) submitBtn.disabled = true; 321 if (btnText) btnText.style.display = 'none'; 322 if (btnSpin) btnSpin.style.display = 'inline-block'; 323 324 try { 325 const data = new FormData(accountSignupForm); 326 const json = await apiPost('signup', Object.fromEntries(data)); 327 328 if (json.status === 'success') { 329 if (isFree) { 330 // Show success step with free account message 331 const msg = document.getElementById('crpw-account-success-msg'); 332 if (msg) msg.textContent = 'Your free account has been created. You can now read complimentary articles.'; 333 const continueBtn = document.getElementById('crpw-account-continue-btn'); 334 if (continueBtn) continueBtn.textContent = 'Continue Reading'; 335 accountShowStep('success'); 336 } else { 337 // Paid plan â redirect to checkout 338 const planId = document.getElementById('crpw-account-plan-id')?.value; 339 window.location.href = (crpwData.checkoutUrl || homeUrl + 'checkout/') + '?level=' + planId; 340 } 341 } else if (json.status === 'verify_email') { 342 // Account created but needs email verification before login 343 const msg = document.getElementById('crpw-account-success-msg'); 344 if (msg) msg.textContent = json.message || 'Please check your email to verify your account before logging in.'; 345 const continueBtn = document.getElementById('crpw-account-continue-btn'); 346 if (continueBtn) { 347 continueBtn.textContent = 'Close'; 348 continueBtn.onclick = () => closeAccountModal(); 349 } 350 accountShowStep('success'); 351 } else { 352 let msg = 'An error occurred. Please try again.'; 353 if (typeof json.message === 'string') { 354 msg = json.message; 355 } else if (json.message?.crm_errors?.length) { 356 msg = json.message.crm_errors.join(' '); 357 } else if (json.message?.required_errors?.length) { 358 msg = json.message.required_errors.join(' '); 359 } 360 if (errBox) { errBox.textContent = msg; errBox.style.display = 'block'; } 361 } 362 } catch (err) { 363 if (errBox) { errBox.textContent = 'A network error occurred. Please try again.'; errBox.style.display = 'block'; } 364 } finally { 365 if (submitBtn) submitBtn.disabled = false;
366 if (btnText) btnText.style.display = ''; 367 if (btnSpin) btnSpin.style.display = 'none'; 368 } 369 }); 370 } 371 372 // ---------------------------------------------------------------- 373 // Account modal â login form 374 // ---------------------------------------------------------------- 375 const accountLoginForm = document.getElementById('crpw-account-login-form'); 376 if (accountLoginForm) { 377 const errBox = document.getElementById('crpw-account-login-error'); 378 const succBox = document.getElementById('crpw-account-login-success'); 379 const submitBtn = document.getElementById('crpw-account-login-btn'); 380 const btnText = submitBtn?.querySelector('.crpw-btn-text'); 381 const btnSpin = submitBtn?.querySelector('.crpw-btn-spinner'); 382 383 accountLoginForm.addEventListener('submit', async function(e) { 384 e.preventDefault(); 385 if (errBox) { errBox.style.display = 'none'; errBox.textContent = ''; } 386 if (submitBtn) submitBtn.disabled = true; 387 if (btnText) btnText.style.display = 'none'; 388 if (btnSpin) btnSpin.style.display = 'inline-block'; 389 390 try { 391 const data = new FormData(accountLoginForm); 392 const json = await apiPost('login', Object.fromEntries(data)); 393 394 if (json.status === 'success') { 395 if (succBox) { succBox.textContent = 'â Login successful! Loadingâ¦'; succBox.style.display = 'block'; } 396 setTimeout(() => { window.location.href = cacheBustUrl( json.next_url || window.location.href ); }, 700); 397 } else if (json.status === 'verify_email') { 398 // Not verified â show message, no redirect 399 if (errBox) { errBox.textContent = json.message || 'Please verify your email address before logging in.'; errBox.style.display = 'block'; } 400 } else { 401 if (errBox) { errBox.textContent = json.message || 'Login failed.'; errBox.style.display = 'block'; } 402 } 403 } catch (err) { 404 if (errBox) { errBox.textContent = 'A network error occurred. Please try again.'; errBox.style.display = 'block'; } 405 } finally { 406 if (submitBtn) submitBtn.disabled = false; 407 if (btnText) btnText.style.display = ''; 408 if (btnSpin) btnSpin.style.display = 'none'; 409 } 410 }); 411 } 412 413 // ---------------------------------------------------------------- 414 // Account modal â continue button (free success step) 415 // ---------------------------------------------------------------- 416 document.getElementById('crpw-account-continue-btn')?.addEventListener('click', () => { 417 closeAccountModal(); 418 window.location.reload(); 419 }); 420 421 // ---------------------------------------------------------------- 422 // Password hint â live feedback on account signup form 423 // ---------------------------------------------------------------- 424 const pwdInput = document.getElementById('crpw-account-password'); 425 const pwdHint = document.getElementById('crpw-account-password-hint'); 426 427 if (pwdInput && pwdHint) { 428 pwdInput.addEventListener('input', function () {
429 const val = pwdInput.value; 430 const hasLetter = /[a-zA-Z]/.test(val); 431 const hasNumSym = /[0-9!@#$%^&*()\-_=+\[\]{};':"\\|,.<>/?]/.test(val); 432 const longEnough = val.length >= 8; 433 const valid = longEnough && hasLetter && hasNumSym; 434 435 pwdHint.style.color = val.length === 0 ? '' : valid ? '#15803d' : '#b91c1c'; 436 }); 437 } 438 439 // ---------------------------------------------------------------- 440 // Email duplicate check â account signup form 441 // ---------------------------------------------------------------- 442 let emailCheckTimer = null; 443 const emailInput = document.getElementById('crpw-account-email'); 444 const emailMsg = document.getElementById('crpw-account-email-msg'); 445 446 if (emailInput && emailMsg) { 447 emailInput.addEventListener('blur', function() { 448 clearTimeout(emailCheckTimer); 449 const email = emailInput.value.trim(); 450 if (!email || !email.includes('@')) return; 451 452 emailCheckTimer = setTimeout(async () => { 453 try { 454 const json = await fetch(restBase + 'check_user?email=' + encodeURIComponent(email), { 455 headers: { 'X-WP-Nonce': nonce } 456 }).then(r => r.json()); 457 458 if (json.exists) { 459 emailMsg.textContent = 'An account with this email already exists. Please log in.'; 460 emailMsg.className = 'crpw-field-msg exists'; 461 } else { 462 emailMsg.textContent = ''; 463 emailMsg.className = 'crpw-field-msg'; 464 } 465 } catch(e) { 466 emailMsg.textContent = ''; 467 } 468 }, 400); 469 }); 470 471 emailInput.addEventListener('input', () => { 472 emailMsg.textContent = ''; 473 emailMsg.className = 'crpw-field-msg'; 474 }); 475 } 476 477 // ================================================================ 478 // Logout form 479 // ================================================================ 480 const logoutForm = document.getElementById('crpw-logout-form'); 481 if (logoutForm) { 482 logoutForm.addEventListener('submit', async function (e) { 483 e.preventDefault(); 484 const redirectTo = logoutForm.querySelector('[name="redirect_to"]')?.value || homeUrl; 485 try { 486 const json = await apiPost('logout', { redirect_to: redirectTo }); 487 window.location.href = json.next_url || homeUrl; 488 } catch (err) { window.location.href = homeUrl; } 489 }); 490 } 491 492 // ================================================================ 493 // Password toggle â handles all instances across all modals 494 // ================================================================ 495 document.querySelectorAll('.crpw-toggle-pw').forEach(function (btn) { 496 const input = btn.closest('.crpw-input-wrap')?.querySelector('input'); 497 if (!input) return; 498 btn.addEventListener('click', function () { 499 const hidden = input.type === 'password'; 500 input.type = hidden ? 'text' : 'password'; 501 btn.setAttribute('aria-label', hidden ? 'Hide password' : 'Show password'); 502 btn.innerHTML = hidden 503 ? '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>' 504 : '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>'; 505 }); 506 }); 507 508})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.