(function attachPhoneAuthModule(root, factory) { root.MultiPagePhoneAuth = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createPhoneAuthModule() { function createPhoneAuthHelpers(deps = {}) { const { fillInput, getActionText, getPageTextSnapshot, getVerificationErrorText, humanPause, isActionEnabled, isAddPhonePageReady, isConsentReady, isPhoneVerificationPageReady, isVisibleElement, performOperationWithDelay: injectedPerformOperationWithDelay, simulateClick, sleep, throwIfStopped, waitForElement, } = deps; const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::'; const PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX = 'PHONE_RESEND_BANNED_NUMBER::'; const PHONE_RESEND_SERVER_ERROR_PREFIX = 'PHONE_RESEND_SERVER_ERROR::'; const PHONE_MAX_USAGE_EXCEEDED_PATTERN = /phone_max_usage_exceeded/i; const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::'; const PHONE_ROUTE_405_RECOVERY_COOLDOWN_MS = 6000; const PHONE_RESEND_ROUTE_405_MAX_RECOVERIES = 2; const PHONE_RESEND_ROUTE_405_MAX_RECOVERY_TOTAL_MS = 12000; const PHONE_RESEND_THROTTLED_PATTERN = /tried\s+to\s+resend\s+too\s+many\s+times|please\s+try\s+again\s+later|too\s+many\s+resend|resend\s+too\s+many|发送.*过于频繁|稍后再试|重试次数过多/i; const PHONE_RESEND_BANNED_NUMBER_PATTERN = /无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|can(?:not|'t)\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number/i; const PHONE_RESEND_SERVER_ERROR_PATTERN = /this\s+page\s+isn['’]?t\s+working|currently\s+unable\s+to\s+handle\s+this\s+request|http\s+error\s+500|500\s+internal\s+server\s+error/i; const PHONE_ROUTE_405_PATTERN = /405\s+method\s+not\s+allowed|route\s+error.*405|did\s+not\s+provide\s+an?\s+[`'"]?action|post\s+request\s+to\s+["']?\/phone-verification/i; const PHONE_ROUTE_405_MAX_RECOVERY_CLICKS = 3; const rootScope = typeof self !== 'undefined' ? self : globalThis; const phoneCountryUtils = rootScope?.MultiPagePhoneCountryUtils || globalThis?.MultiPagePhoneCountryUtils || {}; let lastPhoneRoute405RecoveryFailedAt = 0; let activePhoneResendPromise = null; async function performOperationWithDelay(metadata, operation) { const gate = injectedPerformOperationWithDelay || rootScope?.CodexOperationDelay?.performOperationWithDelay; return typeof gate === 'function' ? gate(metadata, operation) : operation(); } function dispatchInputEvents(element) { if (!element) return; element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); } function normalizePhoneDigits(value) { if (typeof phoneCountryUtils.normalizePhoneDigits === 'function') { return phoneCountryUtils.normalizePhoneDigits(value); } let digits = String(value || '').replace(/\D+/g, ''); if (digits.startsWith('00')) { digits = digits.slice(2); } return digits; } function isExplicitInternationalPhoneInput(value) { return /^\s*(?:\+|00)\s*\d/.test(String(value || '').trim()); } function normalizeCountryLabel(value) { if (typeof phoneCountryUtils.normalizeCountryLabel === 'function') { return phoneCountryUtils.normalizeCountryLabel(value); } return String(value || '') .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .replace(/&/g, ' and ') .replace(/[^\p{L}\p{N}\s]/gu, ' ') .replace(/\s+/g, ' ') .trim() .toLowerCase(); } function getOptionLabel(option) { if (typeof phoneCountryUtils.getOptionLabel === 'function') { return phoneCountryUtils.getOptionLabel(option); } return String(option?.textContent || option?.label || '') .replace(/\s+/g, ' ') .trim(); } function normalizeCountryOptionValue(value) { if (typeof phoneCountryUtils.normalizeCountryOptionValue === 'function') { return phoneCountryUtils.normalizeCountryOptionValue(value); } return String(value || '').trim().toUpperCase(); } function getRegionDisplayName(regionCode, locale) { if (typeof phoneCountryUtils.getRegionDisplayName === 'function') { return phoneCountryUtils.getRegionDisplayName(regionCode, locale); } const normalizedRegionCode = normalizeCountryOptionValue(regionCode); const normalizedLocale = String(locale || '').trim(); if (!/^[A-Z]{2}$/.test(normalizedRegionCode) || !normalizedLocale || typeof Intl?.DisplayNames !== 'function') { return ''; } try { return String( new Intl.DisplayNames([normalizedLocale], { type: 'region' }).of(normalizedRegionCode) || '' ).trim(); } catch { return ''; } } function getCountryOptionMatchLabels(option) { if (typeof phoneCountryUtils.getOptionMatchLabels === 'function') { return phoneCountryUtils.getOptionMatchLabels(option, { document: typeof document !== 'undefined' ? document : null, navigator: rootScope?.navigator || globalThis?.navigator || null, getOptionLabel, }); } const labels = new Set(); const pushLabel = (value) => { const label = String(value || '').replace(/\s+/g, ' ').trim(); if (label) { labels.add(label); } }; pushLabel(getOptionLabel(option)); const regionCode = normalizeCountryOptionValue(option?.value); if (/^[A-Z]{2}$/.test(regionCode)) { pushLabel(regionCode); pushLabel(getRegionDisplayName(regionCode, 'en')); const pageLocale = String( document?.documentElement?.lang || document?.documentElement?.getAttribute?.('lang') || self?.navigator?.language || '' ).trim(); if (pageLocale && !/^en(?:[-_]|$)/i.test(pageLocale)) { pushLabel(getRegionDisplayName(regionCode, pageLocale)); } } return Array.from(labels); } function isSameCountryOption(left, right) { if (!left || !right) { return false; } const leftValue = normalizeCountryOptionValue(left.value); const rightValue = normalizeCountryOptionValue(right.value); if (leftValue && rightValue) { return leftValue === rightValue; } return normalizeCountryLabel(getOptionLabel(left)) === normalizeCountryLabel(getOptionLabel(right)); } function extractDialCodeFromText(value) { if (typeof phoneCountryUtils.extractDialCodeFromText === 'function') { return phoneCountryUtils.extractDialCodeFromText(value); } const match = String(value || '').match(/\(\+\s*(\d{1,4})\s*\)|\+\s*\(\s*(\d{1,4})\s*\)|\+\s*(\d{1,4})\b/); return String(match?.[1] || match?.[2] || match?.[3] || '').trim(); } function getCountryButtonText() { const form = getAddPhoneForm(); if (!form) return ''; const button = form.querySelector('button[aria-haspopup="listbox"]'); if (!button) return ''; const valueNode = button.querySelector('.react-aria-SelectValue'); return String(valueNode?.textContent || button.textContent || '') .replace(/\s+/g, ' ') .trim(); } function getDisplayedDialCode() { const buttonDialCode = extractDialCodeFromText(getCountryButtonText()); if (buttonDialCode) { return buttonDialCode; } const phoneInput = getPhoneInput(); const fieldRoot = phoneInput?.closest('fieldset') || phoneInput?.closest('form') || getAddPhoneForm(); if (!fieldRoot) { return ''; } const visibleSpan = Array.from(fieldRoot.querySelectorAll('span')) .find((element) => isVisibleElement(element) && /^\d{1,4}$/.test(String(element.textContent || '').trim())); return String(visibleSpan?.textContent || '').trim(); } function toNationalPhoneNumber(value, dialCode) { const digits = normalizePhoneDigits(value); const normalizedDialCode = normalizePhoneDigits(dialCode); const isExplicitInternational = isExplicitInternationalPhoneInput(value); if (!digits) { return ''; } if (normalizedDialCode && digits.startsWith(normalizedDialCode) && digits.length > normalizedDialCode.length) { return digits.slice(normalizedDialCode.length); } if (isExplicitInternational) { return digits; } return digits; } function toE164PhoneNumber(value, dialCode) { const digits = normalizePhoneDigits(value); const normalizedDialCode = normalizePhoneDigits(dialCode); const isExplicitInternational = isExplicitInternationalPhoneInput(value); if (!digits) { return ''; } if (isExplicitInternational) { return `+${digits}`; } if (!normalizedDialCode) { return `+${digits}`; } if (digits.startsWith(normalizedDialCode)) { return `+${digits}`; } if (digits.startsWith('0')) { return `+${normalizedDialCode}${digits.slice(1)}`; } return `+${normalizedDialCode}${digits}`; } function getAddPhoneForm() { return document.querySelector('form[action*="/add-phone" i]'); } function getPhoneVerificationForm() { return document.querySelector('form[action*="/phone-verification" i]'); } function getPhoneInput() { const form = getAddPhoneForm(); if (!form) return null; const input = form.querySelector( 'input[type="tel"], input[name="__reservedForPhoneNumberInput_tel"], input[autocomplete="tel"]' ); return input && isVisibleElement(input) ? input : null; } function getHiddenPhoneNumberInput() { const form = getAddPhoneForm(); if (!form) return null; return form.querySelector('input[name="phoneNumber"]'); } function getCountrySelect() { const form = getAddPhoneForm(); if (!form) return null; return form.querySelector('select'); } function getSelectedCountryOption() { const select = getCountrySelect(); if (!select || select.selectedIndex < 0) { return null; } return select.options[select.selectedIndex] || null; } function findCountryOptionByLabel(countryLabel) { const select = getCountrySelect(); if (!select) { return null; } if (typeof phoneCountryUtils.findOptionByCountryLabel === 'function') { return phoneCountryUtils.findOptionByCountryLabel(select.options, countryLabel, { document: typeof document !== 'undefined' ? document : null, navigator: rootScope?.navigator || globalThis?.navigator || null, getOptionLabel, }); } const normalizedTarget = normalizeCountryLabel(countryLabel); if (!normalizedTarget) { return null; } const options = Array.from(select.options); return options.find((option) => ( getCountryOptionMatchLabels(option).some((label) => normalizeCountryLabel(label) === normalizedTarget) )) || options.find((option) => { const normalizedLabels = getCountryOptionMatchLabels(option) .map((label) => normalizeCountryLabel(label)) .filter(Boolean); return normalizedLabels.some((optionLabel) => ( optionLabel.length > 2 && normalizedTarget.length > 2 && (optionLabel.includes(normalizedTarget) || normalizedTarget.includes(optionLabel)) )); }) || null; } function findCountryOptionByPhoneNumber(phoneNumber) { const select = getCountrySelect(); if (!select) { return null; } if (typeof phoneCountryUtils.findOptionByPhoneNumber === 'function') { return phoneCountryUtils.findOptionByPhoneNumber(select.options, phoneNumber, { getOptionLabel }); } const digits = normalizePhoneDigits(phoneNumber); if (!digits) { return null; } let bestMatch = null; let bestDialCodeLength = 0; for (const option of Array.from(select.options || [])) { const dialCode = normalizePhoneDigits(extractDialCodeFromText(getOptionLabel(option))); if (!dialCode || !digits.startsWith(dialCode)) { continue; } if (dialCode.length > bestDialCodeLength) { bestMatch = option; bestDialCodeLength = dialCode.length; } } return bestMatch; } async function trySelectCountryOption(select, targetOption) { if (!select || !targetOption) { return false; } const selectedOption = getSelectedCountryOption(); if (selectedOption && isSameCountryOption(selectedOption, targetOption)) { await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'select', label: 'phone-country-select' }, async () => { dispatchInputEvents(select); }); return true; } await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'select', label: 'phone-country-select' }, async () => { select.value = String(targetOption.value || ''); dispatchInputEvents(select); }); await sleep(250); const nextSelectedOption = getSelectedCountryOption(); return Boolean(nextSelectedOption && isSameCountryOption(nextSelectedOption, targetOption)); } async function ensureCountrySelected(countryLabel, phoneNumber = '') { const select = getCountrySelect(); if (!select) { return false; } const byLabel = findCountryOptionByLabel(countryLabel); if (await trySelectCountryOption(select, byLabel)) { return true; } const byPhoneNumber = findCountryOptionByPhoneNumber(phoneNumber); if (await trySelectCountryOption(select, byPhoneNumber)) { return true; } return Boolean(getSelectedCountryOption()); } function getAddPhoneSubmitButton() { const form = getAddPhoneForm(); if (!form) return null; const buttons = Array.from(form.querySelectorAll('button[type="submit"], input[type="submit"]')); return buttons.find((button) => isVisibleElement(button) && isActionEnabled(button)) || buttons.find((button) => isVisibleElement(button)) || null; } function getPhoneVerificationCodeInput() { const form = getPhoneVerificationForm(); if (!form) return null; const input = form.querySelector( 'input[name="code"], input[autocomplete="one-time-code"], input[inputmode="numeric"]' ); return input && isVisibleElement(input) ? input : null; } function getPhoneVerificationSubmitButton() { const form = getPhoneVerificationForm(); if (!form) return null; const buttons = Array.from(form.querySelectorAll('button[type="submit"], input[type="submit"]')); return buttons.find((button) => { if (!isVisibleElement(button) || !isActionEnabled(button)) return false; const intent = String(button.getAttribute('value') || '').trim().toLowerCase(); if (intent === 'resend') return false; return true; }) || buttons.find((button) => isVisibleElement(button)); } function getPhoneVerificationResendActionText(button) { if (!button) return ''; return [ button.getAttribute?.('value'), button.getAttribute?.('aria-label'), button.getAttribute?.('title'), getActionText(button), button.textContent, ].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(); } function isWhatsAppResendText(value) { return /whats\s*app/i.test(String(value || '')); } function getPhoneVerificationResendActionInfo(button) { const text = getPhoneVerificationResendActionText(button); const channel = isWhatsAppResendText(text) ? 'whatsapp' : (/(?:sms|text\s+message|短信)/i.test(text) ? 'sms' : ''); return { channel, channelText: text, text, }; } function getPhoneVerificationResendButton(options = {}) { const { allowDisabled = false } = options; const form = getPhoneVerificationForm(); if (!form) return null; const buttons = Array.from(form.querySelectorAll('button, input[type="submit"], input[type="button"]')); return buttons.find((button) => { if (!isVisibleElement(button)) return false; if (!allowDisabled && !isActionEnabled(button)) return false; const intent = String(button.getAttribute('value') || '').trim().toLowerCase(); if (intent === 'resend') return true; return /resend|重新发送|再次发送|whats\s*app/i.test(getPhoneVerificationResendActionText(button)); }) || null; } function getPhoneVerificationDisplayedPhone() { const text = getPageTextSnapshot(); const matches = text.match(/\+\d[\d\s-]{6,}\d/g); return matches?.[0] ? matches[0].replace(/\s+/g, ' ').trim() : ''; } function getAddPhoneErrorText() { const form = getAddPhoneForm(); if (!form) { return ''; } const messages = []; const selectors = [ '.react-aria-FieldError', '[slot="errorMessage"]', '[id$="-error"]', '[data-invalid="true"] + *', '[aria-invalid="true"] + *', '[class*="error"]', ]; for (const selector of selectors) { form.querySelectorAll(selector).forEach((el) => { const text = String(el?.textContent || '').replace(/\s+/g, ' ').trim(); if (text) { messages.push(text); } }); } const invalidInput = form.querySelector('input[aria-invalid="true"], input[data-invalid="true"]'); if (invalidInput) { const wrapper = invalidInput.closest('form, [data-rac], div'); const text = String(wrapper?.textContent || '').replace(/\s+/g, ' ').trim(); if (text) { messages.push(text); } } const preferred = messages.find((text) => ( /already|used|linked|eligible|invalid|phone|号码|手机号|错误|失败|try\s+again/i.test(text) )); return preferred || messages[0] || ''; } function getPhoneVerificationInlineMessages() { const form = getPhoneVerificationForm(); if (!form) { return []; } const messages = []; const selectors = [ '.react-aria-FieldError', '[slot="errorMessage"]', '[id$="-error"]', '[data-invalid="true"] + *', '[aria-invalid="true"] + *', '[class*="error"]', ]; for (const selector of selectors) { form.querySelectorAll(selector).forEach((element) => { const text = String(element?.textContent || '').replace(/\s+/g, ' ').trim(); if (text) { messages.push(text); } }); } const verificationError = String(getVerificationErrorText?.() || '').trim(); if (verificationError) { messages.push(verificationError); } return messages; } function getPhoneResendThrottleText() { const inlineMatch = getPhoneVerificationInlineMessages() .find((text) => PHONE_RESEND_THROTTLED_PATTERN.test(text)); if (inlineMatch) { return inlineMatch; } const pageSnapshot = String(getPageTextSnapshot?.() || '').replace(/\s+/g, ' ').trim(); if (pageSnapshot && PHONE_RESEND_THROTTLED_PATTERN.test(pageSnapshot)) { const concise = pageSnapshot.match( /tried\s+to\s+resend\s+too\s+many\s+times[^.。!?]*[.。!?]?|please\s+try\s+again\s+later[^.。!?]*[.。!?]?|发送.*过于频繁[^。!?]*[。!?]?|稍后再试[^。!?]*[。!?]?/i ); return String(concise?.[0] || pageSnapshot).trim(); } return ''; } function getPhoneResendBannedNumberText() { const inlineMatch = getPhoneVerificationInlineMessages() .find((text) => PHONE_RESEND_BANNED_NUMBER_PATTERN.test(text)); if (inlineMatch) { return inlineMatch; } const pageSnapshot = String(getPageTextSnapshot?.() || '').replace(/\s+/g, ' ').trim(); if (pageSnapshot && PHONE_RESEND_BANNED_NUMBER_PATTERN.test(pageSnapshot)) { const concise = pageSnapshot.match( /无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|can(?:not|'t)\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number[^.。!?]*[.。!?]?|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number[^.。!?]*[.。!?]?/i ); return String(concise?.[0] || pageSnapshot).trim(); } return ''; } function getPhoneResendServerErrorText() { const path = String(location?.pathname || ''); if (!/\/contact-verification(?:[/?#]|$)/i.test(path)) { return ''; } const text = String(getPageTextSnapshot?.() || '').replace(/\s+/g, ' ').trim(); const title = String(document?.title || '').replace(/\s+/g, ' ').trim(); const combined = `${title} ${text}`.trim(); if (!PHONE_RESEND_SERVER_ERROR_PATTERN.test(combined)) { return ''; } return combined || 'OpenAI contact-verification page returned HTTP ERROR 500 after resend.'; } function checkPhoneResendError() { const maxUsageText = getAddPhoneErrorText(); if (maxUsageText && PHONE_MAX_USAGE_EXCEEDED_PATTERN.test(maxUsageText)) { return { hasError: true, reason: 'phone_max_usage_exceeded', message: maxUsageText, url: location.href, }; } const bannedNumberText = getPhoneResendBannedNumberText(); if (bannedNumberText) { return { hasError: true, reason: 'resend_phone_banned', prefix: PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX, message: bannedNumberText, url: location.href, }; } const throttledText = getPhoneResendThrottleText(); if (throttledText) { return { hasError: true, reason: 'resend_throttled', prefix: PHONE_RESEND_THROTTLED_ERROR_PREFIX, message: throttledText, url: location.href, }; } const serverErrorText = getPhoneResendServerErrorText(); if (serverErrorText) { return { hasError: true, reason: 'resend_server_error', prefix: PHONE_RESEND_SERVER_ERROR_PREFIX, message: serverErrorText, url: location.href, }; } return { hasError: false, reason: '', message: '', url: location.href, }; } function getAuthRetryButton(options = {}) { const { allowDisabled = false } = options; const direct = document.querySelector('button[data-dd-action-name="Try again"]'); if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) { return direct; } const candidates = document.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]'); return Array.from(candidates).find((element) => { if (!isVisibleElement(element) || (!allowDisabled && !isActionEnabled(element))) { return false; } const text = String(getActionText?.(element) || '').trim(); return /重试|try\s+again/i.test(text); }) || null; } function is405MethodNotAllowedPage() { const path = String(location?.pathname || ''); if (!/\/phone-verification(?:[/?#]|$)/i.test(path) && !/\/add-phone(?:[/?#]|$)/i.test(path)) { return false; } const text = String(getPageTextSnapshot?.() || '').replace(/\s+/g, ' ').trim(); const title = String(document?.title || ''); const matched = PHONE_ROUTE_405_PATTERN.test(text) || PHONE_ROUTE_405_PATTERN.test(title); if (!matched) { return false; } return Boolean(getAuthRetryButton({ allowDisabled: true })); } async function recoverPhoneRoute405(timeout = 12000, options = {}) { const now = Date.now(); if ( lastPhoneRoute405RecoveryFailedAt > 0 && now - lastPhoneRoute405RecoveryFailedAt < PHONE_ROUTE_405_RECOVERY_COOLDOWN_MS ) { throw new Error( `${PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX}Phone verification route is still in 405 recovery cooldown (${Math.ceil((PHONE_ROUTE_405_RECOVERY_COOLDOWN_MS - (now - lastPhoneRoute405RecoveryFailedAt)) / 1000)}s left). URL: ${location.href}` ); } const startedAt = Date.now(); let clicked = 0; const maxRetryClicks = Math.max( 1, Math.floor(Number(options?.maxRetryClicks) || PHONE_ROUTE_405_MAX_RECOVERY_CLICKS) ); while (Date.now() - startedAt < timeout) { throwIfStopped(); if (!is405MethodNotAllowedPage()) { return; } const retryButton = getAuthRetryButton({ allowDisabled: true }); if (retryButton && isActionEnabled(retryButton)) { if (clicked >= maxRetryClicks) { lastPhoneRoute405RecoveryFailedAt = Date.now(); throw new Error( `${PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX}Phone verification route stayed on 405 after ${clicked} retry click(s). URL: ${location.href}` ); } clicked += 1; await humanPause(200, 500); await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'click', label: 'phone-route-retry' }, async () => { simulateClick(retryButton); }); await sleep(1000); continue; } await sleep(250); } lastPhoneRoute405RecoveryFailedAt = Date.now(); throw new Error( `${PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX}Phone verification route 405 recovery timed out after ${clicked} retry click(s). URL: ${location.href}` ); } async function waitForAddPhoneReady(timeout = 20000) { const start = Date.now(); while (Date.now() - start < timeout) { throwIfStopped(); if (isAddPhonePageReady()) { return true; } await sleep(150); } throw new Error('Timed out waiting for add-phone page.'); } async function waitForPhoneVerificationReady(timeout = 20000) { const start = Date.now(); while (Date.now() - start < timeout) { throwIfStopped(); if (is405MethodNotAllowedPage()) { await recoverPhoneRoute405(Math.min(12000, Math.max(1000, timeout - (Date.now() - start)))); continue; } if (isPhoneVerificationPageReady()) { return { phoneVerificationPage: true, displayedPhone: getPhoneVerificationDisplayedPhone(), url: location.href, }; } if (isAddPhonePageReady()) { const errorText = getAddPhoneErrorText(); if (errorText) { return { addPhoneRejected: true, errorText, url: location.href, }; } } await sleep(150); } if (isAddPhonePageReady()) { const errorText = getAddPhoneErrorText(); if (errorText) { return { addPhoneRejected: true, errorText, url: location.href, }; } } throw new Error('Timed out waiting for phone verification page.'); } async function submitPhoneNumber(payload = {}) { const countryLabel = String(payload.countryLabel || '').trim(); const isExplicitInternational = isExplicitInternationalPhoneInput(payload.phoneNumber); await waitForAddPhoneReady(); const countrySelected = await ensureCountrySelected(countryLabel, payload.phoneNumber); if (!countrySelected) { throw new Error(`Failed to select "${countryLabel || 'target country'}" on the add-phone page.`); } const dialCode = getDisplayedDialCode(); if (!dialCode && !isExplicitInternational) { throw new Error(`Could not determine the dial code for "${countryLabel}" on the add-phone page.`); } const phoneNumber = toE164PhoneNumber(payload.phoneNumber, dialCode); const nationalPhoneNumber = toNationalPhoneNumber(payload.phoneNumber, dialCode); if (!phoneNumber || !nationalPhoneNumber) { throw new Error('Missing phone number for add-phone submission.'); } const phoneInput = getPhoneInput() || await waitForElement( 'input[type="tel"], input[name="__reservedForPhoneNumberInput_tel"], input[autocomplete="tel"]', 10000 ); const hiddenPhoneNumberInput = getHiddenPhoneNumberInput(); const submitButton = getAddPhoneSubmitButton(); if (!phoneInput) { throw new Error('Add-phone page is missing the phone number input.'); } if (!submitButton) { throw new Error('Add-phone page is missing the submit button.'); } await humanPause(250, 700); await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'fill', label: 'phone-number' }, async () => { fillInput(phoneInput, nationalPhoneNumber); }); if (hiddenPhoneNumberInput) { await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'hidden-sync', label: 'phone-number-hidden-sync' }, async () => { hiddenPhoneNumberInput.value = phoneNumber; dispatchInputEvents(hiddenPhoneNumberInput); }); } await sleep(250); await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'submit', label: 'phone-number-submit' }, async () => { simulateClick(submitButton); }); return waitForPhoneVerificationReady(); } async function waitForPhoneVerificationOutcome(timeout = 30000) { const start = Date.now(); while (Date.now() - start < timeout) { throwIfStopped(); if (is405MethodNotAllowedPage()) { await recoverPhoneRoute405(Math.min(12000, Math.max(1000, timeout - (Date.now() - start)))); continue; } const errorText = getVerificationErrorText(); if (errorText) { return { invalidCode: true, errorText, url: location.href, }; } if (isConsentReady()) { return { success: true, consentReady: true, url: location.href, }; } if (isAddPhonePageReady()) { return { returnedToAddPhone: true, url: location.href, }; } await sleep(150); } if (isPhoneVerificationPageReady()) { return { invalidCode: true, errorText: getVerificationErrorText() || 'Phone verification page stayed in place after code submission.', url: location.href, }; } return { success: true, assumed: true, url: location.href, }; } async function submitPhoneVerificationCode(payload = {}) { const code = String(payload.code || '').trim(); if (!code) { throw new Error('Missing phone verification code.'); } await waitForPhoneVerificationReady(); const codeInput = getPhoneVerificationCodeInput() || await waitForElement( 'input[name="code"], input[autocomplete="one-time-code"], input[inputmode="numeric"]', 10000 ); const submitButton = getPhoneVerificationSubmitButton(); if (!codeInput) { throw new Error('Phone verification page is missing the code input.'); } if (!submitButton) { throw new Error('Phone verification page is missing the submit button.'); } await humanPause(250, 700); await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'fill', label: 'phone-verification-code' }, async () => { fillInput(codeInput, code); }); await sleep(250); await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'submit', label: 'phone-verification-submit' }, async () => { simulateClick(submitButton); }); if (is405MethodNotAllowedPage()) { await recoverPhoneRoute405(12000); } return waitForPhoneVerificationOutcome(); } async function resendPhoneVerificationCode(timeout = 45000, options = {}) { if (activePhoneResendPromise) { return activePhoneResendPromise; } activePhoneResendPromise = (async () => { const start = Date.now(); const route405RecoveryStart = Date.now(); let route405RecoveryCount = 0; const recoverRoute405WithinResend = async () => { route405RecoveryCount += 1; if (route405RecoveryCount > PHONE_RESEND_ROUTE_405_MAX_RECOVERIES) { throw new Error( `${PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX}Phone verification resend stayed on route-405 page after ${PHONE_RESEND_ROUTE_405_MAX_RECOVERIES} recovery round(s). URL: ${location.href}` ); } const recoveryBudgetLeft = PHONE_RESEND_ROUTE_405_MAX_RECOVERY_TOTAL_MS - (Date.now() - route405RecoveryStart); if (recoveryBudgetLeft <= 0) { throw new Error( `${PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX}Phone verification resend exceeded route-405 recovery budget (${PHONE_RESEND_ROUTE_405_MAX_RECOVERY_TOTAL_MS}ms). URL: ${location.href}` ); } const remainingTimeout = Math.max(1000, timeout - (Date.now() - start)); const recoveryTimeout = Math.max(1000, Math.min(12000, recoveryBudgetLeft, remainingTimeout)); await recoverPhoneRoute405(recoveryTimeout); }; while (Date.now() - start < timeout) { throwIfStopped(); if (is405MethodNotAllowedPage()) { await recoverRoute405WithinResend(); continue; } const bannedNumberText = getPhoneResendBannedNumberText(); if (bannedNumberText) { throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${bannedNumberText}`); } const throttledText = getPhoneResendThrottleText(); if (throttledText) { throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${throttledText}`); } const serverErrorText = getPhoneResendServerErrorText(); if (serverErrorText) { throw new Error(`${PHONE_RESEND_SERVER_ERROR_PREFIX}${serverErrorText}`); } const resendButton = getPhoneVerificationResendButton({ allowDisabled: true }); if (resendButton && isActionEnabled(resendButton)) { const resendInfo = getPhoneVerificationResendActionInfo(resendButton); if (resendInfo.channel === 'whatsapp') { return { resent: false, channel: 'whatsapp', channelText: resendInfo.channelText, text: resendInfo.text, url: location.href, }; } if (options?.probeOnly) { return { resent: false, probed: true, channel: resendInfo.channel || 'unknown', channelText: resendInfo.channelText, text: resendInfo.text, url: location.href, }; } await humanPause(250, 700); await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'click', label: 'phone-verification-resend' }, async () => { simulateClick(resendButton); }); await sleep(1000); if (is405MethodNotAllowedPage()) { await recoverRoute405WithinResend(); continue; } const afterClickBannedNumberText = getPhoneResendBannedNumberText(); if (afterClickBannedNumberText) { throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${afterClickBannedNumberText}`); } const afterClickThrottleText = getPhoneResendThrottleText(); if (afterClickThrottleText) { throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${afterClickThrottleText}`); } const afterClickServerErrorText = getPhoneResendServerErrorText(); if (afterClickServerErrorText) { throw new Error(`${PHONE_RESEND_SERVER_ERROR_PREFIX}${afterClickServerErrorText}`); } return { resent: true, channel: resendInfo.channel || 'sms', channelText: resendInfo.channelText, text: resendInfo.text, url: location.href, }; } await sleep(250); } const timeoutBannedNumberText = getPhoneResendBannedNumberText(); if (timeoutBannedNumberText) { throw new Error(`${PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX}${timeoutBannedNumberText}`); } const timeoutThrottleText = getPhoneResendThrottleText(); if (timeoutThrottleText) { throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${timeoutThrottleText}`); } const timeoutServerErrorText = getPhoneResendServerErrorText(); if (timeoutServerErrorText) { throw new Error(`${PHONE_RESEND_SERVER_ERROR_PREFIX}${timeoutServerErrorText}`); } throw new Error('Timed out waiting for the phone verification resend button.'); })().finally(() => { activePhoneResendPromise = null; }); return activePhoneResendPromise; } async function returnToAddPhone(timeout = 20000) { if (isAddPhonePageReady()) { return { addPhonePage: true, url: location.href, }; } if (!isPhoneVerificationPageReady()) { throw new Error('The auth page is not currently on phone verification or add-phone page.'); } await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'navigation', label: 'phone-return-add-phone' }, async () => { location.assign('/add-phone'); }); await waitForAddPhoneReady(timeout); return { addPhonePage: true, url: location.href, }; } return { getPhoneVerificationDisplayedPhone, checkPhoneResendError, isPhoneVerificationPageReady, resendPhoneVerificationCode, returnToAddPhone, submitPhoneNumber, submitPhoneVerificationCode, toE164PhoneNumber, }; } return { createPhoneAuthHelpers, }; });