feat: Centralize browser instance creation to eliminate duplication in Desktop and Mobile flows

fix: Update error message for HTTP 400 checks to English
refactor: Improve logging for network idle wait timeout in Workers
fix: Initialize lastError in Axios client to prevent undefined errors
chore: Update tsconfig to include path mappings for better module resolution
This commit is contained in:
2025-11-09 17:56:46 +01:00
parent 56aacd3667
commit 9fb5911fa2
10 changed files with 101 additions and 38 deletions

View File

@@ -25,13 +25,15 @@ async function main(): Promise<void> {
}
// Banner
console.log('\n' + '='.repeat(60))
log(false, 'CREATOR-CLI', '', 'log') // Empty line
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'cyan')
log(false, 'CREATOR-CLI', '🚀 Microsoft Account Creator', 'log', 'cyan')
console.log('='.repeat(60))
console.log('\x1b[33m⚠️ DO NOT INTERACT WITH THE BROWSER DURING AUTOMATION\x1b[0m')
console.log('\x1b[33m Everything is fully automated. Any interaction may break the process.\x1b[0m')
console.log('\x1b[33m Only interact when explicitly asked (e.g., CAPTCHA solving).\x1b[0m')
console.log('='.repeat(60) + '\n')
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'cyan')
log(false, 'CREATOR-CLI', '⚠️ DO NOT INTERACT WITH THE BROWSER DURING AUTOMATION', 'warn', 'yellow')
log(false, 'CREATOR-CLI', ' Everything is fully automated. Any interaction may break the process.', 'warn', 'yellow')
log(false, 'CREATOR-CLI', ' Only interact when explicitly asked (e.g., CAPTCHA solving).', 'warn', 'yellow')
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'cyan')
log(false, 'CREATOR-CLI', '', 'log') // Empty line
// Display detected arguments
if (referralUrl) {
@@ -52,7 +54,7 @@ async function main(): Promise<void> {
log(false, 'CREATOR-CLI', '💡 Tip: Use -y flag to auto-accept all prompts', 'log', 'gray')
}
console.log()
log(false, 'CREATOR-CLI', '', 'log') // Empty line
// Create a temporary bot instance to access browser creation
const bot = new MicrosoftRewardsBot(false)
@@ -81,9 +83,10 @@ async function main(): Promise<void> {
if (result) {
// Success banner
console.log('\n' + '='.repeat(60))
log(false, 'CREATOR-CLI', '', 'log') // Empty line
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green')
log(false, 'CREATOR-CLI', '✅ ACCOUNT CREATED SUCCESSFULLY!', 'log', 'green')
console.log('='.repeat(60))
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green')
// Display account details
log(false, 'CREATOR-CLI', `📧 Email: ${result.email}`, 'log', 'cyan')
@@ -95,9 +98,10 @@ async function main(): Promise<void> {
log(false, 'CREATOR-CLI', '🔗 Referral: Linked', 'log', 'green')
}
console.log('='.repeat(60))
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green')
log(false, 'CREATOR-CLI', '💾 Account details saved to accounts-created/ directory', 'log', 'green')
console.log('='.repeat(60) + '\n')
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green')
log(false, 'CREATOR-CLI', '', 'log') // Empty line
// Keep browser open - don't close
log(false, 'CREATOR-CLI', '✅ Account creation complete! Browser will remain open.', 'log', 'green')
@@ -108,9 +112,11 @@ async function main(): Promise<void> {
await new Promise(() => {}) // Never resolves
} else {
// Failure
console.log('\n' + '='.repeat(60))
log(false, 'CREATOR-CLI', '', 'log') // Empty line
log(false, 'CREATOR-CLI', '='.repeat(60), 'error')
log(false, 'CREATOR-CLI', '❌ ACCOUNT CREATION FAILED', 'error')
console.log('='.repeat(60) + '\n')
log(false, 'CREATOR-CLI', '='.repeat(60), 'error')
log(false, 'CREATOR-CLI', '', 'log') // Empty line
await browserContext.close()
process.exit(1)
@@ -118,9 +124,11 @@ async function main(): Promise<void> {
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
console.log('\n' + '='.repeat(60))
log(false, 'CREATOR-CLI', '', 'log') // Empty line
log(false, 'CREATOR-CLI', '='.repeat(60), 'error')
log(false, 'CREATOR-CLI', `❌ Fatal error: ${msg}`, 'error')
console.log('='.repeat(60) + '\n')
log(false, 'CREATOR-CLI', '='.repeat(60), 'error')
log(false, 'CREATOR-CLI', '', 'log') // Empty line
process.exit(1)
}
}

View File

@@ -50,9 +50,10 @@ export default class BrowserFunc {
this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by content text (iteration ${iteration})`, 'error')
return true
}
} catch (e) {
} catch (error) {
// Ignore errors in text check - not critical
this.bot.log(this.bot.isMobile, 'GO-HOME', `Suspension text check skipped: ${e}`, 'warn')
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
@@ -141,7 +142,8 @@ export default class BrowserFunc {
await target.waitForSelector(SELECTORS.MORE_ACTIVITIES, { timeout: TIMEOUTS.DASHBOARD_WAIT }).catch((error) => {
// Continuing is intentional: page may still be functional even if this specific element is missing
// The script extraction will catch any real issues
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Activities element not found after ${TIMEOUTS.DASHBOARD_WAIT}ms timeout, attempting to proceed: ${error instanceof Error ? error.message : String(error)}`, 'warn')
const errorMsg = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Activities element not found after ${TIMEOUTS.DASHBOARD_WAIT}ms timeout, attempting to proceed: ${errorMsg}`, 'warn')
})
let scriptContent = await this.extractDashboardScript(target)
@@ -151,7 +153,10 @@ export default class BrowserFunc {
// Force a navigation retry once before failing hard
await this.goHome(target)
await target.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.VERY_LONG }).catch(logError('BROWSER-FUNC', 'Dashboard recovery load failed', this.bot.isMobile))
await target.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.VERY_LONG }).catch((error) => {
const errorMsg = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'BROWSER-FUNC', `Dashboard recovery load failed: ${errorMsg}`, 'warn')
})
await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM)
scriptContent = await this.extractDashboardScript(target)

View File

@@ -201,7 +201,7 @@ export default class BrowserUtil {
const isNetworkError = $('body.neterror').length
const hasHttp400Error = html.includes('HTTP ERROR 400') ||
html.includes('This page isn\'t working') ||
html.includes('Cette page ne fonctionne pas')
html.includes('This page is not working')
if (isNetworkError || hasHttp400Error) {
const errorType = hasHttp400Error ? 'HTTP 400' : 'network error'

View File

@@ -12,6 +12,7 @@
import type { MicrosoftRewardsBot } from '../index'
import type { Account } from '../interface/Account'
import { createBrowserInstance } from '../util/BrowserFactory'
import { saveSessionData } from '../util/Load'
export interface DesktopFlowResult {
@@ -34,11 +35,8 @@ export class DesktopFlow {
async run(account: Account): Promise<DesktopFlowResult> {
this.bot.log(false, 'DESKTOP-FLOW', 'Starting desktop automation flow')
// FIXED: Use proper typed access instead of unsafe type assertion
const browserModule = await import('../browser/Browser')
const Browser = browserModule.default
const browserInstance = new Browser(this.bot)
const browser = await browserInstance.createBrowser(account.proxy, account.email)
// IMPROVED: Use centralized browser factory to eliminate duplication
const browser = await createBrowserInstance(this.bot, account.proxy, account.email)
let keepBrowserOpen = false

View File

@@ -13,6 +13,7 @@
import type { MicrosoftRewardsBot } from '../index'
import type { Account } from '../interface/Account'
import { createBrowserInstance } from '../util/BrowserFactory'
import { saveSessionData } from '../util/Load'
import { MobileRetryTracker } from '../util/MobileRetryTracker'
@@ -40,11 +41,8 @@ export class MobileFlow {
): Promise<MobileFlowResult> {
this.bot.log(true, 'MOBILE-FLOW', 'Starting mobile automation flow')
// FIXED: Use proper typed access instead of unsafe type assertion
const browserModule = await import('../browser/Browser')
const Browser = browserModule.default
const browserInstance = new Browser(this.bot)
const browser = await browserInstance.createBrowser(account.proxy, account.email)
// IMPROVED: Use centralized browser factory to eliminate duplication
const browser = await createBrowserInstance(this.bot, account.proxy, account.email)
let keepBrowserOpen = false
let browserClosed = false

View File

@@ -177,7 +177,7 @@ export class Login {
const content = await page.content().catch(() => '')
const hasHttp400 = content.includes('HTTP ERROR 400') ||
content.includes('This page isn\'t working') ||
content.includes('Cette page ne fonctionne pas')
content.includes('This page is not working')
if (hasHttp400) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'HTTP 400 detected in content, reloading...', 'warn')
@@ -188,12 +188,20 @@ export class Login {
await this.disableFido(page)
const [, , portalCheck] = await Promise.allSettled([
const [reloadResult, totpResult, portalCheck] = await Promise.allSettled([
this.bot.browser.utils.reloadBadPage(page),
this.tryAutoTotp(page, 'initial landing'),
page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 3000 })
])
// Log any failures for debugging (non-critical)
if (reloadResult.status === 'rejected') {
this.bot.log(this.bot.isMobile, 'LOGIN', `Reload check failed (non-critical): ${reloadResult.reason}`, 'warn')
}
if (totpResult.status === 'rejected') {
this.bot.log(this.bot.isMobile, 'LOGIN', `Auto-TOTP check failed (non-critical): ${totpResult.reason}`, 'warn')
}
await this.checkAccountLocked(page)
const alreadyAuthenticated = portalCheck.status === 'fulfilled'
@@ -293,10 +301,13 @@ export class Login {
if (!recoveryUsed) {
await this.bot.utils.wait(500)
const content = await page.content().catch(() => '')
const content = await page.content().catch((err) => {
this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Failed to get page content for HTTP 400 check: ${err}`, 'warn')
return ''
})
const hasHttp400 = content.includes('HTTP ERROR 400') ||
content.includes('This page isn\'t working') ||
content.includes('Cette page ne fonctionne pas')
content.includes('This page is not working')
if (hasHttp400) {
this.bot.log(this.bot.isMobile, 'LOGIN-APP', 'HTTP 400 detected, reloading...', 'warn')
@@ -435,7 +446,7 @@ export class Login {
const content = await page.content().catch(() => '')
const hasHttp400 = content.includes('HTTP ERROR 400') ||
content.includes('This page isn\'t working') ||
content.includes('Cette page ne fonctionne pas')
content.includes('This page is not working')
if (hasHttp400) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'HTTP 400 on session check, reloading...', 'warn')
@@ -1289,7 +1300,7 @@ export class Login {
const content = await page.content().catch(() => '')
const hasHttp400 = content.includes('HTTP ERROR 400') ||
content.includes('This page isn\'t working') ||
content.includes('Cette page ne fonctionne pas')
content.includes('This page is not working')
if (hasHttp400) {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'HTTP 400 detected during Bing verification, reloading...', 'warn')

View File

@@ -101,7 +101,7 @@ export class Workers {
await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL })
// Wait for new page to load, max 10 seconds, however try regardless in case of error
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { })
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(logError('PUNCH-CARD', 'Network idle wait timeout (non-critical)', this.bot.isMobile))
await this.solveActivities(page, activitiesUncompleted, punchCard)

View File

@@ -87,7 +87,8 @@ class AxiosClient {
return bypassInstance.request(config)
}
let lastError: unknown
// FIXED: Initialize lastError to prevent throwing undefined
let lastError: unknown = new Error('Request failed with unknown error')
const maxAttempts = 2
for (let attempt = 1; attempt <= maxAttempts; attempt++) {

View File

@@ -0,0 +1,33 @@
/**
* Browser Factory Utility
* Eliminates code duplication between Desktop and Mobile flows
*
* Centralized browser instance creation logic
*/
import type { BrowserContext } from 'rebrowser-playwright'
import type { MicrosoftRewardsBot } from '../index'
import type { AccountProxy } from '../interface/Account'
/**
* Create a browser instance for the given account
* IMPROVEMENT: Extracted from DesktopFlow and MobileFlow to eliminate duplication
*
* @param bot Bot instance
* @param proxy Account proxy configuration
* @param email Account email for session naming
* @returns Browser context ready to use
*
* @example
* const browser = await createBrowserInstance(bot, account.proxy, account.email)
*/
export async function createBrowserInstance(
bot: MicrosoftRewardsBot,
proxy: AccountProxy,
email: string
): Promise<BrowserContext> {
const browserModule = await import('../browser/Browser')
const Browser = browserModule.default
const browserInstance = new Browser(bot)
return await browserInstance.createBrowser(proxy, email)
}

View File

@@ -28,6 +28,15 @@
/* Module Resolution Options */
"moduleResolution": "node",
"baseUrl": "./src",
"paths": {
"@constants": ["constants"],
"@interfaces/*": ["interface/*"],
"@utils/*": ["util/*"],
"@browser/*": ["browser/*"],
"@functions/*": ["functions/*"],
"@flows/*": ["flows/*"]
},
"types": ["node"],
"esModuleInterop": true,