import { AxiosRequestConfig } from 'axios' import { CheerioAPI, load } from 'cheerio' import { BrowserContext, Page } from 'rebrowser-playwright' import { RETRY_LIMITS, SELECTORS, TIMEOUTS, URLS } from '../constants' import { MicrosoftRewardsBot } from '../index' import { AppUserData } from '../interface/AppUserData' import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../interface/DashboardData' import { EarnablePoints } from '../interface/Points' import { QuizData } from '../interface/QuizData' import { waitForElementSmart, waitForPageReady } from '../util/browser/SmartWait' import { saveSessionData } from '../util/state/Load' export default class BrowserFunc { private bot: MicrosoftRewardsBot constructor(bot: MicrosoftRewardsBot) { this.bot = bot } /** * Check if account is suspended using multiple detection methods * @param page Playwright page * @param iteration Current iteration number for logging * @returns true if suspended, false otherwise */ private async checkAccountSuspension(page: Page, iteration: number): Promise { // IMPROVED: Smart wait replaces fixed 500ms timeout with adaptive detection const headerResult = await waitForElementSmart(page, SELECTORS.SUSPENDED_ACCOUNT, { initialTimeoutMs: 500, extendedTimeoutMs: 500, state: 'visible' }) const suspendedByHeader = headerResult.found if (suspendedByHeader) { this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by header selector (iteration ${iteration})`, 'error') return true } // Secondary check: look for suspension text in main content area only try { const mainContent = (await page.locator('#contentContainer, #main, .main-content').first().textContent({ timeout: 500 }).catch(() => '')) || '' const suspensionPatterns = [ /account\s+has\s+been\s+suspended/i, /suspended\s+due\s+to\s+unusual\s+activity/i, /your\s+account\s+is\s+temporarily\s+suspended/i ] const isSuspended = suspensionPatterns.some(pattern => pattern.test(mainContent)) if (isSuspended) { this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by content text (iteration ${iteration})`, 'error') return true } } catch (error) { // Ignore errors in text check - not critical const errorMsg = error instanceof Error ? error.message : String(error) this.bot.log(this.bot.isMobile, 'GO-HOME', `Suspension text check skipped: ${errorMsg}`, 'warn') } return false } /** * Navigate the provided page to rewards homepage * @param {Page} page Playwright page */ async goHome(page: Page) { try { const dashboardURL = new URL(this.bot.config.baseURL) if (page.url() === dashboardURL.href) { return } await page.goto(this.bot.config.baseURL) // IMPROVED: Smart page readiness check after navigation // FIXED: Use timeoutMs parameter with increased timeout for slower networks const readyResult = await waitForPageReady(page, { timeoutMs: 15000, // FIXED: 15s timeout to handle slower network conditions logFn: (msg) => this.bot.log(this.bot.isMobile, 'GO-HOME', msg, 'log') }) if (readyResult.timeMs > 8000) { this.bot.log(this.bot.isMobile, 'GO-HOME', `Page took ${readyResult.timeMs}ms to be ready (slow)`, 'warn') } // IMPROVED: Wait for Custom Elements to be registered with proper timeout handling // FIXED: Use Promise.race to enforce actual 5s timeout (Playwright's timeout doesn't work with customElements.whenDefined) try { await Promise.race([ page.evaluate(() => customElements.whenDefined('mee-card-group')), new Promise((_, reject) => setTimeout(() => reject(new Error('Custom element timeout')), 5000)) ]) } catch (error) { // FIXED: Silent fallback - custom element registration is best-effort, not critical // If it times out, we proceed with activities detection anyway this.bot.log(this.bot.isMobile, 'GO-HOME', 'mee-card-group custom element not registered within 5s (non-critical)', 'log') } for (let iteration = 1; iteration <= RETRY_LIMITS.GO_HOME_MAX; iteration++) { await this.bot.utils.wait(500) await this.bot.browser.utils.tryDismissAllMessages(page) // IMPROVED: Try primary selector first with increased timeouts let activitiesResult = await waitForElementSmart(page, SELECTORS.MORE_ACTIVITIES, { initialTimeoutMs: 3000, // FIXED: Increased from 1000ms to 3000ms extendedTimeoutMs: 7000, // FIXED: Increased from 2000ms to 7000ms state: 'attached', logFn: (msg) => this.bot.log(this.bot.isMobile, 'GO-HOME', msg, 'log') }) // IMPROVED: Try fallback selectors if primary fails if (!activitiesResult.found) { this.bot.log(this.bot.isMobile, 'GO-HOME', 'Primary selector failed, trying fallbacks...', 'log') for (const fallbackSelector of SELECTORS.MORE_ACTIVITIES_FALLBACKS) { activitiesResult = await waitForElementSmart(page, fallbackSelector, { initialTimeoutMs: 2000, extendedTimeoutMs: 3000, state: 'attached', logFn: (msg) => this.bot.log(this.bot.isMobile, 'GO-HOME', msg, 'log') }) if (activitiesResult.found) { this.bot.log(this.bot.isMobile, 'GO-HOME', `Found activities using fallback: ${fallbackSelector}`, 'log') break } } } if (activitiesResult.found) { this.bot.log(this.bot.isMobile, 'GO-HOME', 'Visited homepage successfully') break } // Activities not found yet - check if it's because account is suspended const isSuspended = await this.checkAccountSuspension(page, iteration) if (isSuspended) { throw new Error('Account has been suspended!') } // IMPROVED: Enhanced diagnostic logging to identify DOM structure changes if (iteration <= 2) { try { const diagnosticInfo = await page.evaluate(() => { const elementsWithActivitiesId = document.querySelectorAll('[id*="activit"]') const meeCardGroups = document.querySelectorAll('mee-card-group') const hasRoleList = document.querySelectorAll('[role="list"]') const dailySets = document.querySelectorAll('.daily-sets, [data-bi-name="daily-set"]') const rewardsElements = document.querySelectorAll('[class*="rewards"], [id*="rewards"]') const mainContent = document.querySelector('main') return { activitiesIdCount: elementsWithActivitiesId.length, activitiesIds: Array.from(elementsWithActivitiesId).map(el => el.id).slice(0, 5), meeCardGroupCount: meeCardGroups.length, roleListCount: hasRoleList.length, dailySetsCount: dailySets.length, rewardsElementsCount: rewardsElements.length, hasMainContent: !!mainContent, pageTitle: document.title, bodyClasses: document.body.className, url: window.location.href } }) this.bot.log(this.bot.isMobile, 'GO-HOME-DEBUG', 'DOM Diagnostic - ' + `URL: ${diagnosticInfo.url}, ` + `Title: "${diagnosticInfo.pageTitle}", ` + `Elements with 'activit': ${diagnosticInfo.activitiesIdCount} [${diagnosticInfo.activitiesIds.join(', ')}], ` + `mee-card-group: ${diagnosticInfo.meeCardGroupCount}, ` + `role=list: ${diagnosticInfo.roleListCount}, ` + `daily-sets: ${diagnosticInfo.dailySetsCount}, ` + `rewards elements: ${diagnosticInfo.rewardsElementsCount}, ` + `main content: ${diagnosticInfo.hasMainContent}`, 'warn') } catch (error) { this.bot.log(this.bot.isMobile, 'GO-HOME-DEBUG', `Diagnostic failed: ${error}`, 'warn') } } // IMPROVED: Capture screenshot on final iteration for debugging if (iteration === RETRY_LIMITS.GO_HOME_MAX) { try { const fs = await import('fs') const path = await import('path') const debugDir = path.join(process.cwd(), 'debug-screenshots') if (!fs.existsSync(debugDir)) { fs.mkdirSync(debugDir, { recursive: true }) } const timestamp = new Date().toISOString().replace(/[:.]/g, '-') const screenshotPath = path.join(debugDir, `goHome-${this.bot.currentAccountEmail}-${timestamp}.png`) await page.screenshot({ path: screenshotPath, fullPage: true }) this.bot.log(this.bot.isMobile, 'GO-HOME', `Debug screenshot saved: ${screenshotPath}`, 'warn') } catch (error) { this.bot.log(this.bot.isMobile, 'GO-HOME', `Screenshot capture failed: ${error}`, 'warn') } } // Not suspended, just activities not loaded yet - continue to next iteration this.bot.log(this.bot.isMobile, 'GO-HOME', `Activities not found yet (iteration ${iteration}/${RETRY_LIMITS.GO_HOME_MAX}), retrying...`, 'warn') // Below runs if the homepage was unable to be visited const currentURL = new URL(page.url()) if (currentURL.hostname !== dashboardURL.hostname) { await this.bot.browser.utils.tryDismissAllMessages(page) await this.bot.utils.wait(1000) await page.goto(this.bot.config.baseURL) // IMPROVED: Wait for page ready after redirect // FIXED: Use timeoutMs parameter with increased timeout await waitForPageReady(page, { timeoutMs: 15000, // FIXED: 15s timeout to handle slower network conditions logFn: (msg) => this.bot.log(this.bot.isMobile, 'GO-HOME', msg, 'log') }) } else { // FIXED: We're on the right URL but activities not found - force page reload to trigger DOM re-render // This fixes the issue where Tyler needs to manually refresh to see activities this.bot.log(this.bot.isMobile, 'GO-HOME', 'On correct URL but activities missing - forcing page reload to trigger DOM render', 'warn') try { // IMPROVED: Try alternative reload strategies based on iteration if (iteration === 1) { // First attempt: Simple reload await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 }) } else if (iteration === 2) { // Second attempt: Navigate to full dashboard URL (not just base) this.bot.log(this.bot.isMobile, 'GO-HOME', 'Trying full dashboard URL: /rewards/dashboard', 'log') await page.goto(`${this.bot.config.baseURL}/rewards/dashboard`, { waitUntil: 'domcontentloaded', timeout: 15000 }) } else if (iteration === 3) { // Third attempt: Clear localStorage and reload this.bot.log(this.bot.isMobile, 'GO-HOME', 'Clearing localStorage and reloading', 'log') await page.evaluate(() => localStorage.clear()) await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 }) } else { // Final attempts: Hard reload with cache bypass await page.reload({ waitUntil: 'networkidle', timeout: 20000 }) } await waitForPageReady(page, { timeoutMs: 10000, logFn: (msg) => this.bot.log(this.bot.isMobile, 'GO-HOME', msg, 'log') }) // Try scrolling to force lazy-loaded elements to render await page.evaluate(() => { window.scrollTo(0, 200) window.scrollTo(0, 0) }) await this.bot.utils.wait(1000) } catch (reloadError) { const reloadMsg = reloadError instanceof Error ? reloadError.message : String(reloadError) this.bot.log(this.bot.isMobile, 'GO-HOME', `Page reload failed: ${reloadMsg}`, 'warn') } } await this.bot.utils.wait(2000) } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) this.bot.log(this.bot.isMobile, 'GO-HOME', `[goHome] Navigation failed: ${errorMessage}`, 'error') throw error } } /** * Fetch user dashboard data * @returns {DashboardData} Object of user bing rewards dashboard data */ async getDashboardData(page?: Page): Promise { const target = page ?? this.bot.homePage const dashboardURL = new URL(this.bot.config.baseURL) const currentURL = new URL(target.url()) try { // Should never happen since tasks are opened in a new tab! if (currentURL.hostname !== dashboardURL.hostname) { this.bot.log(this.bot.isMobile, 'DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page') await this.goHome(target) } // Reload with retry await this.reloadPageWithRetry(target, 2) // IMPROVED: Smart wait for activities element const activitiesResult = await waitForElementSmart(target, SELECTORS.MORE_ACTIVITIES, { initialTimeoutMs: 3000, extendedTimeoutMs: 7000, state: 'attached', logFn: (msg) => this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', msg, 'log') }) if (!activitiesResult.found) { this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Activities element not found after ${activitiesResult.timeMs}ms, attempting to proceed anyway`, 'warn') } let scriptContent = await this.extractDashboardScript(target) if (!scriptContent) { this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard script not found on first try, attempting recovery', 'warn') // Force a navigation retry once before failing hard await this.goHome(target) // IMPROVED: Smart page readiness check instead of fixed wait // FIXED: Use timeoutMs parameter with increased timeout await waitForPageReady(target, { timeoutMs: 15000, // FIXED: 15s timeout for dashboard recovery logFn: (msg) => this.bot.log(this.bot.isMobile, 'BROWSER-FUNC', msg, 'log') }).catch((error) => { const errorMsg = error instanceof Error ? error.message : String(error) this.bot.log(this.bot.isMobile, 'BROWSER-FUNC', `Dashboard recovery load incomplete: ${errorMsg}`, 'warn') }) scriptContent = await this.extractDashboardScript(target) if (!scriptContent) { this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error') throw new Error('Dashboard data not found within script - check page structure') } } // Extract the dashboard object from the script content const dashboardData = await this.parseDashboardFromScript(target, scriptContent) if (!dashboardData) { this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Unable to parse dashboard script', 'error') throw new Error('Unable to parse dashboard script - inspect recent logs and page markup') } return dashboardData } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `[getDashboardData] Failed to fetch dashboard data: ${errorMessage}`, 'error') throw error } } /** * Reload page with retry logic * FIXED: Added global timeout to prevent infinite retry loops */ private async reloadPageWithRetry(page: Page, maxAttempts: number): Promise { const startTime = Date.now() const MAX_TOTAL_TIME_MS = 30000 // 30 seconds max total let lastError: unknown = null for (let attempt = 1; attempt <= maxAttempts; attempt++) { // Check global timeout if (Date.now() - startTime > MAX_TOTAL_TIME_MS) { this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Reload retry exceeded total timeout (${MAX_TOTAL_TIME_MS}ms)`, 'warn') break } try { await page.reload({ waitUntil: 'domcontentloaded' }) await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM) lastError = null break } catch (re) { lastError = re const msg = (re instanceof Error ? re.message : String(re)) this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Reload failed attempt ${attempt}: ${msg}`, 'warn') if (msg.includes('has been closed')) { if (attempt === 1) { this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn') try { await this.goHome(page) } catch { /* Final recovery attempt - failure is acceptable */ } } else { break } } if (attempt === maxAttempts) { await this.bot.utils.wait(1000) } } } if (lastError) throw lastError } /** * Extract dashboard script from page */ private async extractDashboardScript(page: Page): Promise { return await page.evaluate(() => { const scripts = Array.from(document.querySelectorAll('script')) const dashboardPatterns = ['var dashboard', 'dashboard=', 'dashboard :'] const targetScript = scripts.find(script => { const text = script.innerText return text && dashboardPatterns.some(pattern => text.includes(pattern)) }) return targetScript?.innerText || null }) } /** * Parse dashboard object from script content * IMPROVED: Enhanced validation with structure checks */ private async parseDashboardFromScript(page: Page, scriptContent: string): Promise { return await page.evaluate((scriptContent: string) => { const patterns = [ /var\s+dashboard\s*=\s*(\{[\s\S]*?\});/, /dashboard\s*=\s*(\{[\s\S]*?\});/, /var\s+dashboard\s*:\s*(\{[\s\S]*?\})\s*[,;]/ ] for (const regex of patterns) { const match = regex.exec(scriptContent) if (match && match[1]) { try { const jsonStr = match[1] // Validate basic JSON structure before parsing const trimmed = jsonStr.trim() if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) { continue } const parsed = JSON.parse(jsonStr) // Enhanced validation: check structure and type if (typeof parsed !== 'object' || parsed === null) { continue } // Validate essential dashboard properties exist if (!parsed.userStatus || typeof parsed.userStatus !== 'object') { continue } // Successfully validated dashboard structure return parsed } catch (e) { // JSON.parse failed or validation error - try next pattern continue } } } return null }, scriptContent) } /** * Get search point counters * @returns {Counters} Object of search counter data */ async getSearchPoints(): Promise { const dashboardData = await this.getDashboardData() // Always fetch newest data return dashboardData.userStatus.counters } /** * Get total earnable points with web browser * @returns {number} Total earnable points */ async getBrowserEarnablePoints(): Promise { try { let desktopSearchPoints = 0 let mobileSearchPoints = 0 let dailySetPoints = 0 let morePromotionsPoints = 0 const data = await this.getDashboardData() // Desktop Search Points if (data.userStatus.counters.pcSearch?.length) { data.userStatus.counters.pcSearch.forEach(x => desktopSearchPoints += (x.pointProgressMax - x.pointProgress)) } // Mobile Search Points if (data.userStatus.counters.mobileSearch?.length) { data.userStatus.counters.mobileSearch.forEach(x => mobileSearchPoints += (x.pointProgressMax - x.pointProgress)) } // Daily Set data.dailySetPromotions[this.bot.utils.getFormattedDate()]?.forEach(x => dailySetPoints += (x.pointProgressMax - x.pointProgress)) // More Promotions if (data.morePromotions?.length) { data.morePromotions.forEach(x => { // Only count points from supported activities if (['quiz', 'urlreward'].includes(x.promotionType) && x.exclusiveLockedFeatureStatus !== 'locked') { morePromotionsPoints += (x.pointProgressMax - x.pointProgress) } }) } const totalEarnablePoints = desktopSearchPoints + mobileSearchPoints + dailySetPoints + morePromotionsPoints return { dailySetPoints, morePromotionsPoints, desktopSearchPoints, mobileSearchPoints, totalEarnablePoints } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) this.bot.log(this.bot.isMobile, 'GET-BROWSER-EARNABLE-POINTS', `[getBrowserEarnablePoints] Failed to calculate earnable points: ${errorMessage}`, 'error') throw error } } /** * Get total earnable points with mobile app * @returns {number} Total earnable points */ async getAppEarnablePoints(accessToken: string) { try { const points = { readToEarn: 0, checkIn: 0, totalEarnablePoints: 0 } const eligibleOffers = [ 'ENUS_readarticle3_30points', 'Gamification_Sapphire_DailyCheckIn' ] const data = await this.getDashboardData() // Guard against missing profile/attributes and undefined settings let geoLocale = data?.userProfile?.attributes?.country || 'US' const useGeo = !!(this.bot?.config?.searchSettings?.useGeoLocaleQueries) geoLocale = (useGeo && typeof geoLocale === 'string' && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us' const userDataRequest: AxiosRequestConfig = { url: URLS.APP_USER_DATA, method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}`, 'X-Rewards-Country': geoLocale, 'X-Rewards-Language': 'en' } } const userDataResponse: AppUserData = (await this.bot.axios.request(userDataRequest)).data const userData = userDataResponse.response const eligibleActivities = userData.promotions.filter((x) => eligibleOffers.includes(x.attributes.offerid ?? '')) for (const item of eligibleActivities) { if (item.attributes.type === 'msnreadearn') { points.readToEarn = parseInt(item.attributes.pointmax ?? '', 10) - parseInt(item.attributes.pointprogress ?? '', 10) break } else if (item.attributes.type === 'checkin') { const checkInDay = parseInt(item.attributes.progress ?? '', 10) % 7 const today = new Date() const lastUpdated = new Date(item.attributes.last_updated ?? '') if (checkInDay < 6 && today.getDate() !== lastUpdated.getDate()) { points.checkIn = parseInt(item.attributes['day_' + (checkInDay + 1) + '_points'] ?? '', 10) } break } } points.totalEarnablePoints = points.readToEarn + points.checkIn return points } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) this.bot.log(this.bot.isMobile, 'GET-APP-EARNABLE-POINTS', `[getAppEarnablePoints] Failed to fetch app earnable points: ${errorMessage}`, 'error') throw error } } /** * Get current point amount * @returns {number} Current total point amount */ async getCurrentPoints(): Promise { try { const data = await this.getDashboardData() return data.userStatus.availablePoints } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) this.bot.log(this.bot.isMobile, 'GET-CURRENT-POINTS', `[getCurrentPoints] Failed to fetch current points: ${errorMessage}`, 'error') throw error } } /** * Parse quiz data from provided page * @param {Page} page Playwright page * @returns {QuizData} Quiz data object */ async getQuizData(page: Page): Promise { try { // Wait for page to be fully loaded await page.waitForLoadState('domcontentloaded') await this.bot.utils.wait(TIMEOUTS.MEDIUM) const html = await page.content() const $ = load(html) // Try multiple possible variable names const possibleVariables = [ '_w.rewardsQuizRenderInfo', 'rewardsQuizRenderInfo', '_w.quizRenderInfo', 'quizRenderInfo' ] let scriptContent = '' let foundVariable = '' for (const varName of possibleVariables) { scriptContent = $('script') .toArray() .map(el => $(el).text()) .find(t => t.includes(varName)) || '' if (scriptContent) { foundVariable = varName break } } if (scriptContent && foundVariable) { // Escape dots in variable name for regex const escapedVar = foundVariable.replace(/\./g, '\\.') const regex = new RegExp(`${escapedVar}\\s*=\\s*({.*?});`, 's') const match = regex.exec(scriptContent) if (match && match[1]) { const quizData = JSON.parse(match[1]) this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Found quiz data using variable: ${foundVariable}`, 'log') return quizData } else { this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Variable ${foundVariable} found but could not extract JSON data`, 'error') throw new Error(`Quiz data variable ${foundVariable} found but JSON extraction failed`) } } else { // Log available scripts for debugging const allScripts = $('script') .toArray() .map(el => $(el).text()) .filter(t => t.length > 0) .map(t => t.substring(0, 100)) this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Script not found. Tried variables: ${possibleVariables.join(', ')}`, 'error') this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Found ${allScripts.length} scripts on page`, 'warn') this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'Script containing quiz data not found', 'error') throw new Error('Script containing quiz data not found - check page structure') } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `[getQuizData] Failed to extract quiz data: ${errorMessage}`, 'error') throw error } } async waitForQuizRefresh(page: Page): Promise { try { // IMPROVED: Smart wait replaces fixed 10s timeout with adaptive 2s+5s detection const result = await waitForElementSmart(page, SELECTORS.QUIZ_CREDITS, { initialTimeoutMs: 2000, extendedTimeoutMs: TIMEOUTS.DASHBOARD_WAIT - 2000, state: 'visible', logFn: (msg) => this.bot.log(this.bot.isMobile, 'QUIZ-REFRESH', msg) }) if (!result.found) { this.bot.log(this.bot.isMobile, 'QUIZ-REFRESH', 'Quiz credits element not found', 'error') return false } await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG) return true } catch (error) { this.bot.log(this.bot.isMobile, 'QUIZ-REFRESH', 'An error occurred:' + error, 'error') return false } } async checkQuizCompleted(page: Page): Promise { try { // IMPROVED: Smart wait replaces fixed 2s timeout with adaptive detection const result = await waitForElementSmart(page, SELECTORS.QUIZ_COMPLETE, { initialTimeoutMs: 1000, extendedTimeoutMs: TIMEOUTS.MEDIUM_LONG, state: 'visible', logFn: (msg) => this.bot.log(this.bot.isMobile, 'QUIZ-COMPLETE', msg) }) if (!result.found) { return false } await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG) return true } catch (error) { return false } } async loadInCheerio(page: Page): Promise { const html = await page.content() const $ = load(html) return $ } async getPunchCardActivity(page: Page, activity: PromotionalItem | MorePromotion): Promise { let selector = '' try { const html = await page.content() const $ = load(html) const element = $('.offer-cta').toArray().find((x: unknown) => { const el = x as { attribs?: { href?: string } } return !!el.attribs?.href?.includes(activity.offerId) }) if (element) { selector = `a[href*="${element.attribs.href}"]` } } catch (error) { this.bot.log(this.bot.isMobile, 'GET-PUNCHCARD-ACTIVITY', 'An error occurred:' + error, 'error') } return selector } async closeBrowser(browser: BrowserContext, email: string) { try { // Save cookies await saveSessionData(this.bot.config.sessionPath, browser, email, this.bot.isMobile) await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG) // Close browser await browser.close() this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'Browser closed cleanly!') } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', `[closeBrowser] Failed to close browser cleanly: ${errorMessage}`, 'error') throw error } } }