import type { Page } from 'patchright' import type { MicrosoftRewardsBot } from '../../../index' import { getErrorMessage, promptInput } from './LoginUtils' export class RecoveryLogin { private readonly textInputSelector = '[data-testid="proof-confirmation"]' private readonly maxManualSeconds = 60 private readonly maxManualAttempts = 5 constructor(private bot: MicrosoftRewardsBot) {} private async fillEmail(page: Page, email: string): Promise { try { this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', `Attempting to fill email: ${email}`) const visibleInput = await page .waitForSelector(this.textInputSelector, { state: 'visible', timeout: 500 }) .catch(() => null) if (visibleInput) { await page.keyboard.type(email, { delay: 50 }) await page.keyboard.press('Enter') this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Successfully filled email input field') return true } this.bot.logger.warn( this.bot.isMobile, 'LOGIN-RECOVERY', `Email input field not found with selector: ${this.textInputSelector}` ) return false } catch (error) { this.bot.logger.warn( this.bot.isMobile, 'LOGIN-RECOVERY', `Failed to fill email input: ${error instanceof Error ? error.message : String(error)}` ) return false } } async handle(page: Page, recoveryEmail: string): Promise { try { this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email recovery authentication flow initiated') if (recoveryEmail) { this.bot.logger.info( this.bot.isMobile, 'LOGIN-RECOVERY', `Using provided recovery email: ${recoveryEmail}` ) const filled = await this.fillEmail(page, recoveryEmail) if (!filled) { throw new Error('Email input field not found') } this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Waiting for page response') await this.bot.utils.wait(500) await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { this.bot.logger.debug(this.bot.isMobile, 'LOGIN-RECOVERY', 'Network idle timeout reached') }) const errorMessage = await getErrorMessage(page) if (errorMessage) { throw new Error(`Email verification failed: ${errorMessage}`) } this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email authentication completed successfully') return } this.bot.logger.info( this.bot.isMobile, 'LOGIN-RECOVERY', 'No recovery email provided, will prompt user for input' ) for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) { this.bot.logger.info( this.bot.isMobile, 'LOGIN-RECOVERY', `Starting attempt ${attempt}/${this.maxManualAttempts}` ) this.bot.logger.info( this.bot.isMobile, 'LOGIN-RECOVERY', `Prompting user for email input (timeout: ${this.maxManualSeconds}s)` ) const email = await promptInput({ question: `Recovery email (waiting ${this.maxManualSeconds}s): `, timeoutSeconds: this.maxManualSeconds, validate: email => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) }) if (!email) { this.bot.logger.warn( this.bot.isMobile, 'LOGIN-RECOVERY', `No or invalid email input received (attempt ${attempt}/${this.maxManualAttempts})` ) if (attempt === this.maxManualAttempts) { throw new Error('Manual email input failed: no input received') } continue } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { this.bot.logger.warn( this.bot.isMobile, 'LOGIN-RECOVERY', `Invalid email format received (attempt ${attempt}/${this.maxManualAttempts}) | length=${email.length}` ) if (attempt === this.maxManualAttempts) { throw new Error('Manual email input failed: invalid format') } continue } this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', `Valid email received from user: ${email}`) const filled = await this.fillEmail(page, email) if (!filled) { this.bot.logger.error( this.bot.isMobile, 'LOGIN-RECOVERY', `Failed to fill email input field (attempt ${attempt}/${this.maxManualAttempts})` ) if (attempt === this.maxManualAttempts) { throw new Error('Email input field not found after maximum attempts') } await this.bot.utils.wait(1000) continue } this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Waiting for page response') await this.bot.utils.wait(500) await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { this.bot.logger.debug(this.bot.isMobile, 'LOGIN-RECOVERY', 'Network idle timeout reached') }) const errorMessage = await getErrorMessage(page) if (errorMessage) { this.bot.logger.warn( this.bot.isMobile, 'LOGIN-RECOVERY', `Error from page: "${errorMessage}" (attempt ${attempt}/${this.maxManualAttempts})` ) if (attempt === this.maxManualAttempts) { throw new Error(`Maximum attempts reached. Last error: ${errorMessage}`) } this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Clearing input field for retry') const inputToClear = await page.$(this.textInputSelector).catch(() => null) if (inputToClear) { await inputToClear.click() await page.keyboard.press('Control+A') await page.keyboard.press('Backspace') this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Input field cleared') } else { this.bot.logger.warn(this.bot.isMobile, 'LOGIN-RECOVERY', 'Could not find input field to clear') } await this.bot.utils.wait(1000) continue } this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email authentication completed successfully') return } throw new Error(`Email input failed after ${this.maxManualAttempts} attempts`) } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error) this.bot.logger.error(this.bot.isMobile, 'LOGIN-RECOVERY', `Fatal error: ${errorMsg}`) throw error } } }