PageSourceSearch

http://yicha.net/assets/offer-flow.js?v=1789939077

js yicha.net collected 2026-10-07 06:51:37 UTC 34,725 bytes, 599 lines download raw bytes

1/* v68: offer validation, email verification, and stable submission feedback. */
2(function () {
3    'use strict';
4
5    function noticeTarget(form) {
6        return form.querySelector('[data-offer-notice], [data-purchase-notice], .form-notice, .modal-notice');
7    }
8
9    function standaloneSuccessTarget(form) {
10        if (!form || !form.closest('.standalone-offer')) return null;
11        return form.closest('.standalone-offer').querySelector('[data-offer-success-screen]');
12    }
13
14    function showStandaloneSuccess(form, message) {
15        var screen = standaloneSuccessTarget(form);
16        if (!screen) return false;
17        var layout = form.closest('.sale-layout');
18        var copy = screen.querySelector('[data-offer-success-message]');
19        if (copy) copy.textContent = String(message || 'Thank you! Your offer has been submitted successfully.');
20        if (layout) layout.hidden = true;
21        screen.hidden = false;
22        screen.classList.add('is-visible');
23        window.setTimeout(function () { screen.focus(); }, 0);
24        return true;
25    }
26
27    function showNotice(form, message, failed) {
28        if (!failed && showStandaloneSuccess(form, message)) return;
29        var notice = noticeTarget(form);
30        if (!notice) return;
31        if (notice._offerNoticeTimer) window.clearTimeout(notice._offerNoticeTimer);
32        notice.hidden = false;
33        notice.style.display = 'block';
34        notice.className = notice.matches('.modal-notice')
35            ? 'modal-notice ' + (failed ? 'error' : 'success') + ' is-dismissible'
36            : 'notice ' + (failed ? 'error' : 'success') + ' form-notice is-dismissible';
37        var text = String(message || '').replace(/^[✕✓]\s*/, '');
38        var messageText = document.createElement('span');
39        // Errors are already identified by their color and role. Do not add a
40        // leading cross: it makes longer delivery errors harder to scan.
41        messageText.textContent = failed ? text : '✓ ' + text;
42        var dismiss = document.createElement('button');
43        dismiss.type = 'button';
44        dismiss.className = 'offer-notice-dismiss';
45        dismiss.setAttribute('data-notice-dismiss', '');
46        dismiss.setAttribute('aria-label', 'Dismiss this message');
47        dismiss.textContent = '×';
48        dismiss.addEventListener('click', function () { clearNotice(form); });
49        notice.replaceChildren(messageText, dismiss);
50        notice._offerNoticeVisible = true;
51    }
52
53    function clearNotice(form) {
54        var notice = noticeTarget(form);
55        if (!notice) return;
56        notice.hidden = true;
57        notice.style.display = 'none';
58        notice.textContent = '';
59        notice._offerNoticeVisible = false;
60        if (notice._offerNoticeTimer) window.clearTimeout(notice._offerNoticeTimer);
61        notice._offerNoticeTimer = null;
62        notice.className = notice.matches('.modal-notice') ? 'modal-notice' : 'notice form-notice';
63    }
64
65    window.domainOfferShowNotice = showNotice;
66
67    document.addEventListener('click', function (event) {
68        var close = event.target.closest('[data-offer-close]');
69        if (!close) return;
70        event.preventDefault();
71        var destination = close.getAttribute('data-offer-close-url') || 'index.php';
72        try { window.close(); } catch (ignore) {}
73        // Browsers refuse window.close() for tabs that were not script-opened;
74        // return to the domain page in that case so the completed form cannot
75        // be submitted again accidentally.
76        window.setTimeout(function () { window.location.assign(destination); }, 120);
77    });
78
79    // Some legacy standalone handlers reset the form immediately after
80    // rendering a success message.  Preserve that message across the native
81    // reset event so a successful offer is never reported without feedback.
82    document.addEventListener('reset', function (event) {
83        var form = event.target;
84        if (!form || !form.matches || !form.matches('form[data-ajax="offer"], form[data-offer-form]')) return;
85        var notice = noticeTarget(form);
86        if (!notice || !notice._offerNoticeVisible) return;
87        var html = notice.innerHTML;
88        var className = notice.className;
89        window.setTimeout(function () {
90            if (!notice._offerNoticeVisible) return;
91            notice.hidden = false;
92            notice.style.display = 'block';
93            notice.className = className;
94            if (!notice.innerHTML.trim()) notice.innerHTML = html;
95        }, 0);
96    }, true);
97
98    function decorateVisibleNotice(notice) {
99        if (!notice || notice.hidden || !String(notice.textContent || '').trim() || notice.querySelector('.offer-notice-dismiss')) return;
100        if (window.getComputedStyle && window.getComputedStyle(notice).display === 'none') return;
101        notice.classList.add('is-dismissible');
102        var dismiss = document.createElement('button');
103        dismiss.type = 'button';
104        dismiss.className = 'offer-notice-dismiss';
105        dismiss.setAttribute('data-notice-dismiss', '');
106        dismiss.setAttribute('aria-label', 'Dismiss this message');
107        dismiss.textContent = '×';
108        notice.appendChild(dismiss);
109    }
110
111    // Also make any server-rendered notice on these public pages dismissible.
112    document.querySelectorAll('.notice').forEach(decorateVisibleNotice);
113
114    // Notices are injected after page load, so keep one delegated close handler
115    // for both the standalone form and the homepage modal.
116    document.addEventListener('click', function (event) {
117        var dismiss = event.target.closest('[data-notice-dismiss], .offer-notice-dismiss');
118        if (!dismiss) return;
119        var notice = dismiss.closest('.modal-notice, .form-notice, [data-email-code-notice], .notice');
120        var form = dismiss.closest('form');
121        if (form && noticeTarget(form) === notice) clearNotice(form);
122        else if (notice) {
123            notice.hidden = true;
124            notice.style.display = 'none';
125            notice.replaceChildren();
126            notice.classList.remove('is-dismissible');
127        }
128    });
129
130    function entryElements(form) {
131        var grid = form.querySelector('.modal-form-grid');
132        if (grid) return [grid];
133        return Array.prototype.slice.call(form.children).filter(function (node) {
134            return node.classList && (node.classList.contains('field') || node.classList.contains('interest-confirmation'));
135        });
136    }
137
138    function setEntryVisible(form, visible) {
139        entryElements(form).forEach(function (element) {
140            element.classList.toggle('offer-entry-hidden', !visible);
141            element.setAttribute('aria-hidden', visible ? 'false' : 'true');
142        });
143    }
144
145    function panelElements(form) {
146        var panel = form.querySelector('[data-offer-email-verification-panel]');
147        return {
148            panel: panel,
149            input: panel && panel.querySelector('[name="email_verification_code"]'),
150            title: panel && panel.querySelector('[data-offer-verification-title]'),
151            copy: panel && panel.querySelector('[data-offer-verification-copy]'),
152            email: panel && panel.querySelector('[data-offer-verification-email]'),
153            codeField: panel && panel.querySelector('[data-offer-code-field]'),
154            verify: panel && panel.querySelector('[data-offer-verify-submit]'),
155            resend: panel && panel.querySelector('[data-resend-offer-code]'),
156            countdown: panel && panel.querySelector('[data-email-code-notice]'),
157            change: panel && panel.querySelector('[data-change-offer-details]'),
158            another: panel && panel.querySelector('[data-new-offer]'),
159            main: form.querySelector('[data-offer-main-submit]')
160        };
161    }
162
163    function restoreMainSubmit(parts) {
164        if (!parts || !parts.main) return;
165        parts.main.hidden = false;
166        parts.main.disabled = false;
167        parts.main.textContent = 'Submit Offer';
168    }
169
170    function clearVerificationCountdown(parts) {
171        if (!parts || !parts.panel) return;
172        if (parts.panel._verificationCountdown) window.clearInterval(parts.panel._verificationCountdown);
173        parts.panel._verificationCountdown = null;
174        delete parts.panel.dataset.verificationExpiresAt;
175    }
176
177    function formatRemaining(seconds) {
178        var total = Math.max(0, Math.ceil(seconds));
179        return String(Math.floor(total / 60)).padStart(2, '0') + ':' + String(total % 60).padStart(2, '0');
180    }
181
182    function setVerificationCountdown(parts, seconds) {
183        if (!parts) return;
184        if (seconds > 0) {
185            if (parts.resend) {
186                parts.resend.disabled = true;
187                parts.resend.textContent = 'Resend available in ' + formatRemaining(seconds);
188            }
189            if (parts.countdown) {
190                parts.countdown.hidden = false;
191                parts.countdown.textContent = 'Code expires in ' + formatRemaining(seconds) + '. You can request a new code after 5 minutes.';
192            }
193        } else {
194            if (parts.resend) {
195                parts.resend.disabled = false;
196                parts.resend.textContent = 'Resend code';
197            }
198            if (parts.countdown) {
199                parts.countdown.hidden = false;
200                parts.countdown.textContent = 'The code has expired. You can now request a new code.';
201            }
202        }
203    }
204
205    function startVerificationCountdown(form, expiresAt) {
206        var parts = panelElements(form);
207        if (!parts.panel) return;
208        clearVerificationCountdown(parts);
209        var expiry = Number(expiresAt || 0);
210        if (!Number.isFinite(expiry) || expiry <= Date.now() / 1000) expiry = Math.floor(Date.now() / 1000) + 300;
211        parts.panel.dataset.verificationExpiresAt = String(expiry);
212        var tick = function () {
213            var remaining = expiry - Date.now() / 1000;
214            setVerificationCountdown(parts, remaining);
215            if (remaining <= 0) clearVerificationCountdown(parts);
216        };
217        tick();
218        if (expiry > Date.now() / 1000) parts.panel._verificationCountdown = window.setInterval(tick, 1000);
219    }
220
221    function rememberPanelCopy(parts) {
222        if (!parts.panel || parts.panel.dataset.copyRemembered === '1') return;
223        parts.panel.dataset.copyRemembered = '1';
224        parts.panel.dataset.defaultTitle = parts.title ? parts.title.textContent : '';
225        parts.panel.dataset.defaultCopy = parts.copy ? parts.copy.textContent : '';
226    }
227
228    function restoreVerificationEntry(form, resetForm, preserveNotice) {
229        var parts = panelElements(form);
230        if (!parts.panel) return;
231        rememberPanelCopy(parts);
232        clearVerificationCountdown(parts);
233        if (!preserveNotice) clearNotice(form);
234        if (resetForm) form.reset();
235        form.dataset.offerVerificationStage = '';
236        setEntryVisible(form, true);
237        parts.panel.hidden = true;
238        parts.panel.classList.remove('is-complete');
239        if (parts.input) { parts.input.value = ''; parts.input.disabled = true; parts.input.required = false; }
240        if (parts.title) parts.title.textContent = parts.panel.dataset.defaultTitle || 'Enter the code we sent you';
241        if (parts.copy) parts.copy.textContent = parts.panel.dataset.defaultCopy || 'Enter the verification code from your email.';
242        if (parts.codeField) parts.codeField.hidden = false;
243        if (parts.verify) { parts.verify.hidden = true; parts.verify.disabled = false; }
244        if (parts.resend) { parts.resend.hidden = false; parts.resend.disabled = false; parts.resend.textContent = 'Resend code'; }
245        if (parts.change) parts.change.hidden = false;
246        if (parts.another) parts.another.hidden = true;
247        if (parts.countdown) { parts.countdown.hidden = true; parts.countdown.textContent = ''; }
248        restoreMainSubmit(parts);
249    }
250
251    window.domainOfferVerificationStart = function (form, response) {
252        if (!form || form.dataset.offerEmailVerification !== '1') return;
253        var parts = panelElements(form);
254        if (!parts.panel) return;
255        rememberPanelCopy(parts);
256        var email = form.querySelector('[data-match-group="email"]');
257        form.dataset.offerVerificationStage = 'pending';
258        setEntryVisible(form, true);
259        parts.panel.hidden = false;
260        parts.panel.classList.remove('is-complete');
261        if (parts.email) parts.email.textContent = email ? email.value.trim() : '';
262        if (parts.input) {
263            parts.input.value = '';
264            parts.input.disabled = false;
265            parts.input.required = true;
266            window.setTimeout(function () { parts.input.focus(); }, 0);
267        }
268        if (parts.codeField) parts.codeField.hidden = false;
269        if (parts.verify) { parts.verify.hidden = true; parts.verify.disabled = false; }
270        if (parts.resend) parts.resend.hidden = false;
271        if (parts.change) parts.change.hidden = false;
272        if (parts.another) parts.another.hidden = true;
273        restoreMainSubmit(parts);
274        clearNotice(form);
275        startVerificationCountdown(form, response && response.verification_expires_at);
276    };
277
278    window.domainOfferVerificationFinish = function (form) {
279        // Keep the success message visible after the optional verification
280        // panel is reset for the next offer.
281        if (form) restoreVerificationEntry(form, false, true);
282    };
283
284    function offerCompleted(form, response) {
285        var parts = panelElements(form);
286        if (!parts.panel) return;
287        clearVerificationCountdown(parts);
288        form.dataset.offerVerificationStage = 'complete';
289        /* Keep the PT12 form intact.  The result is a compact success notice,
290           not a separate completed-card screen. */
291        setEntryVisible(form, true);
292        parts.panel.hidden = true;
293        parts.panel.classList.remove('is-complete');
294        if (parts.input) { parts.input.value = ''; parts.input.disabled = true; parts.input.required = false; }
295        if (parts.title) parts.title.textContent = parts.panel.dataset.defaultTitle || 'Enter the code we sent you';
296        if (parts.copy) parts.copy.textContent = parts.panel.dataset.defaultCopy || 'Enter the verification code from your email.';
297        if (parts.codeField) parts.codeField.hidden = false;
298        if (parts.verify) { parts.verify.hidden = true; parts.verify.disabled = false; }
299        if (parts.resend) { parts.resend.hidden = false; parts.resend.disabled = false; parts.resend.textContent = 'Resend code'; }
300        if (parts.change) parts.change.hidden = false;
301        if (parts.another) parts.another.hidden = true;
302        // A successful verification must never leave the submit control stuck
303        // on the transient “Verifying…” label, even while the completed state
304        // is being rendered or the form is prepared for another submission.
305        if (parts.main) { parts.main.hidden = true; parts.main.disabled = false; parts.main.textContent = 'Submit Offer'; }
306        if (parts.countdown) { parts.countdown.hidden = true; parts.countdown.textContent = ''; }
307        var question = form.querySelector('[data-verification-question]');
308        if (question && response.verification_question) question.textContent = response.verification_question;
309        showNotice(form, response.message || 'Thank you! Your offer has been submitted successfully.', false);
310    }
311
312    function submitVerification(form) {
313        var parts = panelElements(form);
314        if (!parts.input || !parts.input.checkValidity()) { if (parts.input) parts.input.reportValidity(); return; }
315        var verify = parts.main || parts.verify;
316        if (verify) { verify.disabled = true; verify.textContent = 'Verifying…'; }
317        fetch(form.action, {method: 'POST', body: new FormData(form), headers: {'Accept': 'application/json'}})
318            .then(function (response) { return response.json(); })
319            .then(function (response) {
320                if (!response.ok) { showNotice(form, response.message || 'The verification code is incorrect.', true); return; }
321                offerCompleted(form, response);
322            })
323            .catch(function () { showNotice(form, 'Unable to verify the code. Please try again.', true); })
324            .finally(function () {
325                // Always restore the control after the request.  On success it
326                // is hidden by offerCompleted(), but resetting the label here
327                // prevents “Verifying…” from surviving into the next attempt.
328                if (verify) {
329                    verify.disabled = false;
330                    verify.textContent = 'Submit Offer';
331                }
332            });
333    }
334
335    function resendCode(form, button) {
336        var parts = panelElements(form);
337        var expiresAt = parts.panel ? Number(parts.panel.dataset.verificationExpiresAt || 0) : 0;
338        if (expiresAt > Date.now() / 1000) {
339            setVerificationCountdown(parts, expiresAt - Date.now() / 1000);
340            return;
341        }
342        var email = form.querySelector('[data-match-group="email"]');
343        var csrf = form.querySelector('[name="csrf"]');
344        var domain = form.querySelector('[name="domain"]');
345        if (!email || !email.checkValidity()) { if (email) email.focus(); showNotice(form, 'Please enter a valid email address first.', true); return; }
346        var body = new URLSearchParams();
347        body.set('csrf', csrf ? csrf.value : '');
348        body.set('email', email.value.trim());
349        body.set('domain', domain ? domain.value : '');
350        body.set('verification_flow', 'offer');
351        button.disabled = true;
352        button.textContent = 'Sending…';
353        fetch(form.dataset.emailCodeEndpoint || 'api/send_offer_verification.php', {method: 'POST', headers: {'Content-Type':'application/x-www-form-urlencoded', 'Accept':'application/json'}, body: body.toString()})
354            .then(function (response) { return response.json(); })
355            .then(function (response) {
356                if (!response.ok) {
357                    showNotice(form, response.message || 'The email service rejected the resend request. Please try again later.', true);
358                    if (response.verification_expires_at) startVerificationCountdown(form, response.verification_expires_at);
359                    return;
360                }
361                clearNotice(form);
362                startVerificationCountdown(form, response.verification_expires_at);
363            })
364            .catch(function () { showNotice(form, 'The verification request could not reach the server. Please check your connection and try again.', true); })
365            .finally(function () {
366                if (form.dataset.offerVerificationStage === 'pending' && (!parts.panel || Number(parts.panel.dataset.verificationExpiresAt || 0) <= Date.now() / 1000)) {
367                    button.disabled = false;
368                    button.textContent = 'Resend code';
369                }
370            });
371    }
372
373    document.addEventListener('submit', function (event) {
374        var form = event.target.closest('form[data-offer-email-verification="1"]');
375        if (!form || form.dataset.offerVerificationStage !== 'pending') return;
376        event.preventDefault();
377        event.stopImmediatePropagation();
378        submitVerification(form);
379    }, true);
380
381    document.addEventListener('click', function (event) {
382        var resend = event.target.closest('[data-resend-offer-code]');
383        if (resend) { var resendForm = resend.closest('form'); if (resendForm) resendCode(resendForm, resend); return; }
384        var change = event.target.closest('[data-change-offer-details]');
385        if (change) { var changeForm = change.closest('form'); if (changeForm) restoreVerificationEntry(changeForm, false); return; }
386        var another = event.target.closest('[data-new-offer]');
387        if (another) { var anotherForm = another.closest('form'); if (anotherForm) restoreVerificationEntry(anotherForm, true); }
388    });
389
390    function flagUrl(iso) { return 'https://flagcdn.com/24x18/' + String(iso || '').toLowerCase() + '.png'; }
391    function appendFlag(container, record, className) {
392        var holder = document.createElement('span');
393        holder.className = className || 'phone-country-flag-art';
394        var image = document.createElement('img');
395        image.src = flagUrl(record.iso);
396        image.alt = record.country + ' flag';
397        image.width = 24; image.height = 18; image.loading = 'lazy';
398        var fallback = document.createElement('span');
399        fallback.className = 'phone-country-emoji-fallback';
400        fallback.textContent = record.flag || '🌐'; fallback.hidden = true;
401        image.addEventListener('error', function () { image.hidden = true; fallback.hidden = false; });
402        holder.appendChild(image); holder.appendChild(fallback); container.appendChild(holder);
403        return holder;
404    }
405
406    function createCountryOption(record, choose) {
407        var option = document.createElement('button');
408        option.type = 'button'; option.className = 'phone-country-option'; option.dataset.phoneSearch = record.search;
409        appendFlag(option, record, 'phone-country-option-flag');
410        var code = document.createElement('span'); code.className = 'phone-country-option-code'; code.textContent = record.code;
411        var name = document.createElement('span'); name.className = 'phone-country-option-name'; name.textContent = record.country;
412        option.appendChild(code); option.appendChild(name);
413        option.addEventListener('click', function () { choose(record); });
414        return option;
415    }
416
417    function enhanceCountryPicker(select) {
418        var composite = select.closest('[data-phone-composite]');
419        if (!composite || composite.dataset.countryPickerReady === '1') return;
420        composite.dataset.countryPickerReady = '1';
421        Array.prototype.slice.call(composite.querySelectorAll('[data-phone-flag]')).forEach(function (item) { item.remove(); });
422        var records = Array.prototype.slice.call(select.options).map(function (option, index) {
423            var code = option.value; var country = option.dataset.country || option.textContent.trim(); var iso = option.dataset.iso || '';
424            return {index:index, code:code, country:country, iso:iso, flag:option.dataset.flag || '🌐', search:(country + ' ' + code + ' ' + code.replace('+', '') + ' ' + iso).toLowerCase()};
425        }).sort(function (a, b) {
426            var left = parseInt(a.code.replace('+', ''), 10); var right = parseInt(b.code.replace('+', ''), 10);
427            return left === right ? a.country.localeCompare(b.country) : left - right;
428        });
429        select.classList.add('phone-country-native'); select.tabIndex = -1; select.setAttribute('aria-hidden', 'true');
430        var picker = document.createElement('button');
431        picker.type = 'button'; picker.className = 'phone-country-picker'; picker.setAttribute('aria-haspopup', 'listbox'); picker.setAttribute('aria-expanded', 'false');
432        var selectedArt = document.createElement('span'); selectedArt.className = 'phone-country-selected-art';
433        var selectedCode = document.createElement('span'); selectedCode.className = 'phone-country-picker-code';
434        var arrow = document.createElement('span'); arrow.className = 'phone-country-picker-arrow'; arrow.setAttribute('aria-hidden', 'true'); arrow.textContent = '⌄';
435        picker.appendChild(selectedArt); picker.appendChild(selectedCode); picker.appendChild(arrow);
436        var menu = document.createElement('div'); menu.className = 'phone-country-menu'; menu.hidden = true;
437        var search = document.createElement('input'); search.type = 'search'; search.className = 'phone-country-search'; search.placeholder = 'Search country or code'; search.setAttribute('aria-label', 'Search country or country code');
438        var optionsBox = document.createElement('div'); optionsBox.className = 'phone-country-options'; optionsBox.setAttribute('role', 'listbox');
439        menu.appendChild(search); menu.appendChild(optionsBox);
440        function selectedRecord() { return records.filter(function (record) { return record.index === select.selectedIndex; })[0] || records[0]; }
441        function updateSelected() {
442            var record = selectedRecord(); if (!record) return;
443            selectedArt.replaceChildren(); appendFlag(selectedArt, record, 'phone-country-selected-flag');
444            selectedCode.textContent = record.code; picker.title = record.country + ' ' + record.code;
445        }
446        function closeMenu() { menu.hidden = true; picker.setAttribute('aria-expanded', 'false'); }
447        function choose(record) { select.selectedIndex = record.index; select.dispatchEvent(new Event('change', {bubbles:true})); updateSelected(); closeMenu(); }
448        records.forEach(function (record) { optionsBox.appendChild(createCountryOption(record, choose)); });
449        picker.addEventListener('click', function (event) {
450            event.preventDefault(); event.stopPropagation();
451            var opening = menu.hidden;
452            document.querySelectorAll('.phone-country-menu:not([hidden])').forEach(function (other) { other.hidden = true; });
453            document.querySelectorAll('.phone-country-picker[aria-expanded="true"]').forEach(function (other) { other.setAttribute('aria-expanded', 'false'); });
454            menu.hidden = !opening; picker.setAttribute('aria-expanded', opening ? 'true' : 'false');
455            if (opening) { search.value = ''; Array.prototype.forEach.call(optionsBox.children, function (item) { item.hidden = false; }); window.setTimeout(function () { search.focus(); }, 0); }
456        });
457        search.addEventListener('input', function () {
458            var needle = search.value.trim().toLowerCase();
459            var firstMatch = null;
460            Array.prototype.forEach.call(optionsBox.children, function (option) {
461                var matched = needle === '' || option.dataset.phoneSearch.indexOf(needle) !== -1;
462                option.hidden = !matched;
463                if (matched && !firstMatch) firstMatch = option;
464            });
465            if (firstMatch) {
466                optionsBox.scrollTop = Math.max(0, firstMatch.offsetTop - 2);
467                firstMatch.classList.add('is-search-match');
468            }
469            Array.prototype.forEach.call(optionsBox.children, function (option) { if (option !== firstMatch) option.classList.remove('is-search-match'); });
470        });
471        search.addEventListener('keydown', function (event) { if (event.key === 'Escape') { closeMenu(); picker.focus(); } });
472        select.addEventListener('change', updateSelected);
473        composite.insertBefore(picker, select); composite.appendChild(menu); updateSelected();
474    }
475
476    document.querySelectorAll('[data-phone-code]').forEach(enhanceCountryPicker);
477    document.addEventListener('click', function (event) {
478        if (!event.target.closest('.whatsapp-composite')) {
479            document.querySelectorAll('.phone-country-menu:not([hidden])').forEach(function (menu) { menu.hidden = true; });
480            document.querySelectorAll('.phone-country-picker[aria-expanded="true"]').forEach(function (picker) { picker.setAttribute('aria-expanded', 'false'); });
481        }
482    });
483    document.addEventListener('keydown', function (event) {
484        if (event.key !== 'Escape') return;
485        document.querySelectorAll('.phone-country-menu:not([hidden])').forEach(function (menu) { menu.hidden = true; });
486        document.querySelectorAll('.phone-country-picker[aria-expanded="true"]').forEach(function (picker) { picker.setAttribute('aria-expanded', 'false'); });
487    });
488})();
489
490/* v71: fixed-price and offer verification use separate persisted flows while
491   keeping the existing purchase.php continuation response. */
492(function () {
493    'use strict';
494    function parts(form) {
495        var panel = form.querySelector('[data-purchase-email-verification-panel]');
496        return {
497            panel: panel,
498            input: panel && panel.querySelector('[data-purchase-verification-input]'),
499            email: panel && panel.querySelector('[data-purchase-verification-email]'),
500            resend: panel && panel.querySelector('[data-resend-purchase-code]'),
501            countdown: panel && panel.querySelector('[data-purchase-email-code-notice]'),
502            main: form.querySelector('[data-purchase-main-submit]')
503        };
504    }
505    function purchaseNotice(form, message, failed) {
506        var notice = form.querySelector('[data-purchase-notice], .form-notice');
507        if (!notice) return;
508        notice.hidden = false; notice.style.display = 'block';
509        notice.className = (notice.matches('.modal-notice') ? 'modal-notice ' : 'notice form-notice ') + (failed ? 'error' : 'success') + ' is-dismissible';
510        var copy = document.createElement('span');
511        copy.textContent = String(message || '').replace(/^[✕✓]\s*/, '');
512        var close = document.createElement('button');
513        close.type = 'button'; close.className = 'offer-notice-dismiss'; close.textContent = '×';
514        close.setAttribute('data-notice-dismiss', ''); close.setAttribute('aria-label', 'Dismiss this message');
515        notice.replaceChildren(copy, close);
516    }
517    function remainingLabel(seconds) {
518        var total = Math.max(0, Math.ceil(seconds));
519        return String(Math.floor(total / 60)).padStart(2, '0') + ':' + String(total % 60).padStart(2, '0');
520    }
521    function stopCountdown(item) {
522        if (!item || !item.panel) return;
523        if (item.panel._purchaseVerificationCountdown) window.clearInterval(item.panel._purchaseVerificationCountdown);
524        item.panel._purchaseVerificationCountdown = null;
525    }
526    function startCountdown(form, expiresAt) {
527        var item = parts(form);
528        if (!item.panel) return;
529        stopCountdown(item);
530        var expiry = Number(expiresAt || 0);
531        if (!Number.isFinite(expiry) || expiry <= Date.now() / 1000) expiry = Math.floor(Date.now() / 1000) + 300;
532        item.panel.dataset.verificationExpiresAt = String(expiry);
533        var tick = function () {
534            var left = expiry - Date.now() / 1000;
535            if (item.resend) {
536                item.resend.disabled = left > 0;
537                item.resend.textContent = left > 0 ? 'Resend available in ' + remainingLabel(left) : 'Resend code';
538            }
539            if (item.countdown) {
540                item.countdown.hidden = false;
541                item.countdown.textContent = left > 0 ? 'Code expires in ' + remainingLabel(left) + '.' : 'The code has expired. You can now request a new code.';
542            }
543            if (left <= 0) stopCountdown(item);
544        };
545        tick();
546        if (expiry > Date.now() / 1000) item.panel._purchaseVerificationCountdown = window.setInterval(tick, 1000);
547    }
548    window.domainPurchaseVerificationStart = function (form, response) {
549        if (!form || form.dataset.purchaseEmailVerification !== '1') return;
550        var item = parts(form);
551        if (!item.panel) return;
552        form.dataset.purchaseVerificationStage = 'pending';
553        item.panel.hidden = false;
554        if (item.email) {
555            var email = form.querySelector('[data-match-group="email"]');
556            item.email.textContent = email ? email.value.trim() : '';
557        }
558        if (item.input) {
559            item.input.disabled = false; item.input.required = true; item.input.value = '';
560            window.setTimeout(function () { item.input.focus(); }, 0);
561        }
562        if (item.main) { item.main.disabled = false; item.main.textContent = 'Verify & Continue →'; }
563        var notice = form.querySelector('[data-purchase-notice], .form-notice');
564        if (notice) { notice.hidden = true; notice.style.display = 'none'; notice.replaceChildren(); }
565        startCountdown(form, response && response.verification_expires_at);
566    };
567    function resend(form, button) {
568        var item = parts(form);
569        var expiresAt = item.panel ? Number(item.panel.dataset.verificationExpiresAt || 0) : 0;
570        if (expiresAt > Date.now() / 1000) return;
571        var email = form.querySelector('[data-match-group="email"]');
572        var csrf = form.querySelector('[name="csrf"]');
573        var domain = form.querySelector('[name="domain"]');
574        if (!email || !email.checkValidity()) { if (email) email.focus(); purchaseNotice(form, 'Please enter a valid email address first.', true); return; }
575        var body = new URLSearchParams();
576        body.set('csrf', csrf ? csrf.value : ''); body.set('email', email.value.trim()); body.set('domain', domain ? domain.value : ''); body.set('verification_flow', 'purchase');
577        button.disabled = true; button.textContent = 'Sending…';
578        fetch(form.dataset.emailCodeEndpoint || 'api/send_offer_verification.php', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded','Accept':'application/json'}, body:body.toString()})
579            .then(function (response) { return response.json(); })
580            .then(function (response) {
581                if (!response.ok) { purchaseNotice(form, response.message || 'The email service rejected the resend request. Please try again later.', true); if (response.verification_expires_at) startCountdown(form, response.verification_expires_at); return; }
582                startCountdown(form, response.verification_expires_at);
583            })
584            .catch(function () { purchaseNotice(form, 'The verification request could not reach the server. Please check your connection and try again.', true); })
585            .finally(function () { if (!item.panel || Number(item.panel.dataset.verificationExpiresAt || 0) <= Date.now() / 1000) { button.disabled = false; button.textContent = 'Resend code'; } });
586    }
587    document.addEventListener('click', function (event) {
588        var resendButton = event.target.closest('[data-resend-purchase-code]');
589        if (resendButton) { var resendForm = resendButton.closest('form'); if (resendForm) resend(resendForm, resendButton); return; }
590        var changeButton = event.target.closest('[data-change-purchase-details]');
591        if (!changeButton) return;
592        var form = changeButton.closest('form'); if (!form) return;
593        var item = parts(form); form.dataset.purchaseVerificationStage = '';
594        stopCountdown(item);
595        if (item.panel) item.panel.hidden = true;
596        if (item.input) { item.input.value = ''; item.input.disabled = true; item.input.required = false; }
597        if (item.main) { item.main.disabled = false; item.main.textContent = 'Continue to Platform →'; }
598    });
599})();

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.