import fs from 'fs' import path from 'path' import * as readline from 'readline' import type { BrowserContext, Page } from 'rebrowser-playwright' import { log } from '../util/Logger' import { DataGenerator } from './DataGenerator' import { CreatedAccount } from './types' export class AccountCreator { private page!: Page private dataGenerator: DataGenerator private referralUrl?: string private rl: readline.Interface private rlClosed = false constructor(referralUrl?: string) { this.referralUrl = referralUrl this.dataGenerator = new DataGenerator() this.rl = readline.createInterface({ input: process.stdin, output: process.stdout }) this.rlClosed = false } // Human-like delay helper private async humanDelay(minMs: number, maxMs: number): Promise { const delay = Math.random() * (maxMs - minMs) + minMs await this.page.waitForTimeout(Math.floor(delay)) } /** * UTILITY: Find first visible element from list of selectors * Reserved for future use - simplifies selector fallback logic * * Usage example: * const element = await this.findFirstVisible(['selector1', 'selector2'], 'CONTEXT') * if (element) await element.click() */ /* private async findFirstVisible(selectors: string[], context: string): Promise | null> { for (const selector of selectors) { try { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `[${context}] Found element: ${selector}`, 'log', 'green') return element } } catch { continue } } log(false, 'CREATOR', `[${context}] No visible element found`, 'warn', 'yellow') return null } */ /** * UTILITY: Retry an async operation with exponential backoff */ private async retryOperation( operation: () => Promise, context: string, maxRetries: number = 3, initialDelayMs: number = 1000 ): Promise { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { log(false, 'CREATOR', `[${context}] Attempt ${attempt}/${maxRetries}`, 'log', 'cyan') const result = await operation() log(false, 'CREATOR', `[${context}] ✅ Success on attempt ${attempt}`, 'log', 'green') return result } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `[${context}] Attempt ${attempt} failed: ${msg}`, 'warn', 'yellow') if (attempt < maxRetries) { const delayMs = initialDelayMs * Math.pow(2, attempt - 1) log(false, 'CREATOR', `[${context}] Retrying in ${delayMs}ms...`, 'log', 'yellow') await this.page.waitForTimeout(delayMs) } else { log(false, 'CREATOR', `[${context}] ❌ All attempts failed`, 'error') return null } } } return null } /** * CRITICAL: Wait for dropdown to be fully closed before continuing */ private async waitForDropdownClosed(context: string, maxWaitMs: number = 5000): Promise { log(false, 'CREATOR', `[${context}] Waiting for dropdown to close...`, 'log', 'cyan') const startTime = Date.now() while (Date.now() - startTime < maxWaitMs) { // Check if any dropdown menu is visible const dropdownSelectors = [ 'div[role="listbox"]', 'ul[role="listbox"]', 'div[role="menu"]', 'ul[role="menu"]', '[class*="dropdown"][class*="open"]' ] let anyVisible = false for (const selector of dropdownSelectors) { const visible = await this.page.locator(selector).first().isVisible().catch(() => false) if (visible) { anyVisible = true break } } if (!anyVisible) { log(false, 'CREATOR', `[${context}] ✅ Dropdown closed`, 'log', 'green') return true } await this.page.waitForTimeout(500) } log(false, 'CREATOR', `[${context}] ⚠️ Dropdown may still be open`, 'warn', 'yellow') return false } /** * CRITICAL: Verify input value after filling */ private async verifyInputValue( selector: string, expectedValue: string, context: string ): Promise { try { const input = this.page.locator(selector).first() const actualValue = await input.inputValue().catch(() => '') if (actualValue === expectedValue) { log(false, 'CREATOR', `[${context}] ✅ Input value verified: ${expectedValue}`, 'log', 'green') return true } else { log(false, 'CREATOR', `[${context}] ⚠️ Value mismatch: expected "${expectedValue}", got "${actualValue}"`, 'warn', 'yellow') return false } } catch (error) { log(false, 'CREATOR', `[${context}] ⚠️ Could not verify input value`, 'warn', 'yellow') return false } } /** * CRITICAL: Verify no errors are displayed on the page * Returns true if no errors found, false if errors present */ private async verifyNoErrors(context: string): Promise { log(false, 'CREATOR', `[${context}] Checking for error messages...`, 'log', 'cyan') const errorSelectors = [ 'div[id*="Error"]', 'div[id*="error"]', 'div[class*="error"]', 'div[role="alert"]', '[aria-invalid="true"]', 'span[class*="error"]', '.error-message', '[data-bind*="errorMessage"]' ] for (const selector of errorSelectors) { try { const errorElement = this.page.locator(selector).first() const isVisible = await errorElement.isVisible().catch(() => false) if (isVisible) { const errorText = await errorElement.textContent().catch(() => 'Unknown error') log(false, 'CREATOR', `[${context}] ❌ Error detected: ${errorText}`, 'error') return false } } catch { continue } } log(false, 'CREATOR', `[${context}] ✅ No errors detected`, 'log', 'green') return true } /** * CRITICAL: Verify page transition was successful * Checks that new elements appeared AND old elements disappeared * Reserved for future use - can be called for complex page transitions * * Usage example: * const success = await this.verifyPageTransition( * 'EMAIL_TO_PASSWORD', * ['input[type="password"]'], * ['input[type="email"]'] * ) * if (!success) return null */ /* private async verifyPageTransition( context: string, expectedNewSelectors: string[], expectedGoneSelectors: string[], timeoutMs: number = 15000 ): Promise { log(false, 'CREATOR', `[${context}] Verifying page transition...`, 'log', 'cyan') const startTime = Date.now() try { // STEP 1: Wait for at least ONE new element to appear log(false, 'CREATOR', `[${context}] Waiting for new page elements...`, 'log', 'cyan') let newElementFound = false for (const selector of expectedNewSelectors) { try { const element = this.page.locator(selector).first() await element.waitFor({ timeout: Math.min(5000, timeoutMs), state: 'visible' }) log(false, 'CREATOR', `[${context}] ✅ New element appeared: ${selector}`, 'log', 'green') newElementFound = true break } catch { continue } } if (!newElementFound) { log(false, 'CREATOR', `[${context}] ❌ No new elements appeared - transition likely failed`, 'error') return false } // STEP 2: Verify old elements are gone log(false, 'CREATOR', `[${context}] Verifying old elements disappeared...`, 'log', 'cyan') await this.humanDelay(1000, 2000) // Give time for old elements to disappear for (const selector of expectedGoneSelectors) { try { const element = this.page.locator(selector).first() const stillVisible = await element.isVisible().catch(() => false) if (stillVisible) { log(false, 'CREATOR', `[${context}] ⚠️ Old element still visible: ${selector}`, 'warn', 'yellow') // Don't fail immediately - element might be animating out } else { log(false, 'CREATOR', `[${context}] ✅ Old element gone: ${selector}`, 'log', 'green') } } catch { // Element not found = good, it's gone log(false, 'CREATOR', `[${context}] ✅ Old element not found: ${selector}`, 'log', 'green') } } // STEP 3: Verify no errors on new page const noErrors = await this.verifyNoErrors(`${context}_TRANSITION`) if (!noErrors) { log(false, 'CREATOR', `[${context}] ❌ Errors found after transition`, 'error') return false } const elapsed = Date.now() - startTime log(false, 'CREATOR', `[${context}] ✅ Page transition verified (${elapsed}ms)`, 'log', 'green') return true } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `[${context}] ❌ Page transition verification failed: ${msg}`, 'error') return false } } */ /** * CRITICAL: Verify that a click action was successful * Checks that something changed after the click (URL, visible elements, etc.) * Reserved for future use - can be called for complex click verifications * * Usage example: * await button.click() * const success = await this.verifyClickSuccess('BUTTON_CLICK', true, ['div.new-content']) * if (!success) return null */ /* private async verifyClickSuccess( context: string, urlShouldChange: boolean = false, expectedNewSelectors: string[] = [] ): Promise { log(false, 'CREATOR', `[${context}] Verifying click was successful...`, 'log', 'cyan') const startUrl = this.page.url() // Wait a bit for changes to occur await this.humanDelay(2000, 3000) // Check 1: URL change (if expected) if (urlShouldChange) { const newUrl = this.page.url() if (newUrl === startUrl) { log(false, 'CREATOR', `[${context}] ⚠️ URL did not change (might be intentional)`, 'warn', 'yellow') } else { log(false, 'CREATOR', `[${context}] ✅ URL changed: ${startUrl} → ${newUrl}`, 'log', 'green') return true } } // Check 2: New elements appeared (if expected) if (expectedNewSelectors.length > 0) { for (const selector of expectedNewSelectors) { try { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `[${context}] ✅ New element appeared: ${selector}`, 'log', 'green') return true } } catch { continue } } log(false, 'CREATOR', `[${context}] ⚠️ No expected elements appeared`, 'warn', 'yellow') } // Check 3: No errors appeared const noErrors = await this.verifyNoErrors(`${context}_CLICK`) if (!noErrors) { log(false, 'CREATOR', `[${context}] ❌ Errors appeared after click`, 'error') return false } log(false, 'CREATOR', `[${context}] ✅ Click appears successful`, 'log', 'green') return true } */ private async askQuestion(question: string): Promise { return new Promise((resolve) => { this.rl.question(question, (answer) => { resolve(answer.trim()) }) }) } /** * CRITICAL: Wait for page to be completely stable before continuing * Checks for loading spinners, network activity, URL stability, and JS execution */ private async waitForPageStable(context: string, maxWaitMs: number = 30000): Promise { log(false, 'CREATOR', `[${context}] Waiting for page to be stable...`, 'log', 'cyan') const startTime = Date.now() const startUrl = this.page.url() try { // STEP 1: Wait for network to be idle await this.page.waitForLoadState('networkidle', { timeout: maxWaitMs }) log(false, 'CREATOR', `[${context}] ✅ Network idle`, 'log', 'green') // STEP 2: Wait for DOM to be fully loaded await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {}) // STEP 3: Check document.readyState const readyState = await this.page.evaluate(() => document.readyState).catch(() => 'unknown') log(false, 'CREATOR', `[${context}] Document readyState: ${readyState}`, 'log', 'cyan') // STEP 4: Additional delay to ensure everything is rendered and JS executed await this.humanDelay(3000, 5000) // Check for loading indicators const loadingSelectors = [ '.loading', '[class*="spinner"]', '[class*="loading"]', '[aria-busy="true"]', 'div:has-text("Loading")', 'div:has-text("Chargement")', 'div:has-text("Please wait")', 'div:has-text("Veuillez patienter")', 'div:has-text("Creating")', 'div:has-text("Création")' ] // Wait for all loading indicators to disappear for (const selector of loadingSelectors) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `[${context}] Loading indicator detected: ${selector}`, 'log', 'yellow') try { await element.waitFor({ state: 'hidden', timeout: maxWaitMs - (Date.now() - startTime) }) log(false, 'CREATOR', `[${context}] ✅ Loading indicator gone`, 'log', 'green') } catch { log(false, 'CREATOR', `[${context}] ⚠️ Loading indicator still present`, 'warn', 'yellow') } } } // Verify URL hasn't changed during wait (unless we expect it to) const endUrl = this.page.url() if (startUrl !== endUrl) { log(false, 'CREATOR', `[${context}] URL changed: ${startUrl} → ${endUrl}`, 'log', 'yellow') } const elapsed = Date.now() - startTime log(false, 'CREATOR', `[${context}] ✅ Page is stable (waited ${elapsed}ms)`, 'log', 'green') return true } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `[${context}] ⚠️ Page stability check failed: ${msg}`, 'warn', 'yellow') return false } } /** * CRITICAL: Wait for Microsoft account creation to complete * This happens AFTER CAPTCHA and can take several seconds */ private async waitForAccountCreation(): Promise { log(false, 'CREATOR', '⏳ Waiting for Microsoft to create the account...', 'log', 'cyan') const maxWaitTime = 60000 // 60 seconds const startTime = Date.now() try { // STEP 1: Wait for any "Creating account" messages to appear AND disappear const creationMessages = [ 'text="Creating your account"', 'text="Création de votre compte"', 'text="Setting up your account"', 'text="Configuration de votre compte"', 'text="Please wait"', 'text="Veuillez patienter"' ] for (const messageSelector of creationMessages) { const element = this.page.locator(messageSelector).first() const visible = await element.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `Account creation message detected: "${messageSelector}"`, 'log', 'yellow') // Wait for this message to disappear try { await element.waitFor({ state: 'hidden', timeout: 45000 }) log(false, 'CREATOR', '✅ Account creation message gone', 'log', 'green') } catch { log(false, 'CREATOR', '⚠️ Creation message still present after 45s', 'warn', 'yellow') } } } // STEP 2: Wait for URL to stabilize or change to expected page log(false, 'CREATOR', 'Waiting for navigation to complete...', 'log', 'cyan') let urlStableCount = 0 let lastUrl = this.page.url() while (Date.now() - startTime < maxWaitTime) { await this.humanDelay(1000, 1500) const currentUrl = this.page.url() if (currentUrl === lastUrl) { urlStableCount++ // URL has been stable for 3 consecutive checks if (urlStableCount >= 3) { log(false, 'CREATOR', `✅ URL stable at: ${currentUrl}`, 'log', 'green') break } } else { log(false, 'CREATOR', `URL changed: ${lastUrl} → ${currentUrl}`, 'log', 'yellow') lastUrl = currentUrl urlStableCount = 0 } } // STEP 3: Wait for page to be fully loaded await this.waitForPageStable('ACCOUNT_CREATION', 15000) // STEP 4: Additional safety delay await this.humanDelay(3000, 5000) const elapsed = Date.now() - startTime log(false, 'CREATOR', `✅ Account creation complete (waited ${elapsed}ms)`, 'log', 'green') return true } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `⚠️ Account creation wait failed: ${msg}`, 'warn', 'yellow') return false } } /** * CRITICAL: Verify that an element exists, is visible, and is interactable */ private async verifyElementReady( selector: string, context: string, timeoutMs: number = 10000 ): Promise { try { const element = this.page.locator(selector).first() // Wait for element to exist await element.waitFor({ timeout: timeoutMs, state: 'attached' }) // Wait for element to be visible await element.waitFor({ timeout: 5000, state: 'visible' }) // Check if element is enabled (for buttons/inputs) const isEnabled = await element.isEnabled().catch(() => true) if (!isEnabled) { log(false, 'CREATOR', `[${context}] Element found but disabled: ${selector}`, 'warn', 'yellow') return false } log(false, 'CREATOR', `[${context}] ✅ Element ready: ${selector}`, 'log', 'green') return true } catch (error) { log(false, 'CREATOR', `[${context}] Element not ready: ${selector}`, 'warn', 'yellow') return false } } async create(context: BrowserContext): Promise { try { this.page = await context.newPage() log(false, 'CREATOR', 'Starting account creation process...', 'log', 'cyan') // Navigate to signup page await this.navigateToSignup() // Click "Create account" button await this.clickCreateAccount() // Generate email and fill it (handles suggestions automatically) const emailResult = await this.generateAndFillEmail() if (!emailResult) { log(false, 'CREATOR', 'Failed to configure email', 'error') return null } log(false, 'CREATOR', `Email accepted: ${emailResult}`, 'log', 'green') // Wait for password page and fill it const password = await this.fillPassword() if (!password) { log(false, 'CREATOR', 'Failed to generate password', 'error') return null } // Click Next button const passwordNextSuccess = await this.clickNext('password') if (!passwordNextSuccess) { log(false, 'CREATOR', '❌ Failed to proceed after password step', 'error') return null } // Extract final email from identity badge to confirm const finalEmail = await this.extractEmail() const confirmedEmail = finalEmail || emailResult log(false, 'CREATOR', `Using email: ${confirmedEmail}`, 'log', 'green') // Fill birthdate const birthdate = await this.fillBirthdate() if (!birthdate) { log(false, 'CREATOR', 'Failed to fill birthdate', 'error') return null } // Click Next button const birthdateNextSuccess = await this.clickNext('birthdate') if (!birthdateNextSuccess) { log(false, 'CREATOR', '❌ Failed to proceed after birthdate step', 'error') return null } // Fill name fields const names = await this.fillNames(confirmedEmail) if (!names) { log(false, 'CREATOR', 'Failed to fill names', 'error') return null } // Click Next button const namesNextSuccess = await this.clickNext('names') if (!namesNextSuccess) { log(false, 'CREATOR', '❌ Failed to proceed after names step', 'error') return null } // Wait for CAPTCHA page const captchaDetected = await this.waitForCaptcha() if (captchaDetected) { log(false, 'CREATOR', '⚠️ CAPTCHA detected - waiting for human to solve it...', 'warn', 'yellow') log(false, 'CREATOR', 'Please solve the CAPTCHA in the browser. The script will wait...', 'log', 'yellow') await this.waitForCaptchaSolved() log(false, 'CREATOR', '✅ CAPTCHA solved! Continuing...', 'log', 'green') } // Handle post-CAPTCHA questions (Stay signed in, etc.) await this.handlePostCreationQuestions() // Navigate to Bing Rewards and verify connection await this.verifyAccountActive() // Create account object const createdAccount: CreatedAccount = { email: confirmedEmail, password: password, birthdate: { day: birthdate.day, month: birthdate.month, year: birthdate.year }, firstName: names.firstName, lastName: names.lastName, createdAt: new Date().toISOString(), referralUrl: this.referralUrl } // Save to file await this.saveAccount(createdAccount) log(false, 'CREATOR', `✅ Account created successfully: ${confirmedEmail}`, 'log', 'green') return createdAccount } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `Error during account creation: ${msg}`, 'error') log(false, 'CREATOR', '⚠️ Browser left open for inspection. Press Ctrl+C to exit.', 'warn', 'yellow') return null } finally { try { if (!this.rlClosed) { this.rl.close() this.rlClosed = true } } catch {/* ignore */} } } private async navigateToSignup(): Promise { if (this.referralUrl) { log(false, 'CREATOR', `Navigating to referral URL: ${this.referralUrl}`, 'log', 'cyan') await this.page.goto(this.referralUrl, { waitUntil: 'networkidle', timeout: 60000 }) await this.waitForPageStable('REFERRAL_PAGE', 20000) await this.humanDelay(2000, 3000) log(false, 'CREATOR', 'Looking for "Join Microsoft Rewards" button...', 'log') const joinButtonSelectors = [ 'a#start-earning-rewards-link', 'a.cta.learn-more-btn', 'a[href*="signup"]', 'button[class*="join"]' ] let clicked = false for (const selector of joinButtonSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { await button.click() await this.humanDelay(2000, 3000) await this.waitForPageStable('AFTER_JOIN_CLICK', 15000) log(false, 'CREATOR', `✅ Clicked join button with selector: ${selector}`, 'log', 'green') clicked = true break } } if (!clicked) { log(false, 'CREATOR', 'Could not find join button, continuing anyway...', 'warn', 'yellow') } } else { const url = 'https://login.live.com/' log(false, 'CREATOR', `No referral URL - navigating to: ${url}`, 'log', 'cyan') await this.page.goto(url, { waitUntil: 'networkidle', timeout: 60000 }) await this.waitForPageStable('LOGIN_PAGE', 20000) await this.humanDelay(2000, 3000) } } private async clickCreateAccount(): Promise { log(false, 'CREATOR', 'Looking for "Create account" button...', 'log') await this.waitForPageStable('BEFORE_CREATE_ACCOUNT', 15000) const createAccountSelectors = [ 'a[id*="signup"]', 'a[href*="signup"]', 'span[role="button"].fui-Link', 'button[id*="signup"]', 'a[data-testid*="signup"]' ] for (const selector of createAccountSelectors) { const button = this.page.locator(selector).first() try { await button.waitFor({ timeout: 5000 }) await button.click() await this.humanDelay(2000, 3000) await this.waitForPageStable('AFTER_CREATE_ACCOUNT', 15000) log(false, 'CREATOR', `✅ Clicked "Create account" with selector: ${selector}`, 'log', 'green') return } catch { continue } } throw new Error('Could not find "Create account" button with any selector') } private async generateAndFillEmail(): Promise { log(false, 'CREATOR', '\n=== Email Configuration ===', 'log', 'cyan') await this.waitForPageStable('EMAIL_PAGE', 15000) const useAutoGenerate = await this.askQuestion('Generate email automatically? (Y/n): ') let email: string if (useAutoGenerate.toLowerCase() === 'n' || useAutoGenerate.toLowerCase() === 'no') { email = await this.askQuestion('Enter your email: ') log(false, 'CREATOR', `Using custom email: ${email}`, 'log', 'cyan') } else { email = this.dataGenerator.generateEmail() log(false, 'CREATOR', `Generated realistic email: ${email}`, 'log', 'cyan') } const emailInput = this.page.locator('input[type="email"]').first() await emailInput.waitFor({ timeout: 15000 }) // CRITICAL: Retry fill with SMART verification // Microsoft separates username from domain for outlook.com/hotmail.com addresses const emailFillSuccess = await this.retryOperation( async () => { await emailInput.clear() await this.humanDelay(800, 1500) await emailInput.fill(email) await this.humanDelay(1200, 2500) // SMART VERIFICATION: Check if Microsoft separated the domain const inputValue = await emailInput.inputValue().catch(() => '') const emailUsername = email.split('@')[0] // e.g., "sharon_jackson" const emailDomain = email.split('@')[1] // e.g., "outlook.com" // Check if input contains full email OR just username (Microsoft separated domain) if (inputValue === email) { // Full email is in input (not separated) log(false, 'CREATOR', `[EMAIL_INPUT] ✅ Input value verified: ${email}`, 'log', 'green') return true } else if (inputValue === emailUsername && (emailDomain === 'outlook.com' || emailDomain === 'hotmail.com' || emailDomain === 'outlook.fr')) { // Microsoft separated the domain - this is EXPECTED and OK log(false, 'CREATOR', `[EMAIL_INPUT] ✅ Username verified: ${emailUsername} (domain separated by Microsoft)`, 'log', 'green') return true } else { // Unexpected value log(false, 'CREATOR', `[EMAIL_INPUT] ⚠️ Unexpected value: expected "${email}" or "${emailUsername}", got "${inputValue}"`, 'warn', 'yellow') throw new Error('Email input value not verified') } }, 'EMAIL_FILL', 3, 1000 ) if (!emailFillSuccess) { log(false, 'CREATOR', 'Failed to fill email after retries', 'error') return null } log(false, 'CREATOR', 'Clicking Next button...', 'log') const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() await nextBtn.waitFor({ timeout: 15000 }) // CRITICAL: Get current URL before clicking const urlBeforeClick = this.page.url() await nextBtn.click() await this.humanDelay(2000, 3000) await this.waitForPageStable('AFTER_EMAIL_SUBMIT', 20000) // CRITICAL: Verify the click had an effect log(false, 'CREATOR', 'Verifying email submission was processed...', 'log', 'cyan') const urlAfterClick = this.page.url() if (urlBeforeClick === urlAfterClick) { // URL didn't change - check if there's an error or if we're on password page const onPasswordPage = await this.page.locator('input[type="password"]').first().isVisible().catch(() => false) const hasError = await this.page.locator('div[id*="Error"], div[role="alert"]').first().isVisible().catch(() => false) if (!onPasswordPage && !hasError) { log(false, 'CREATOR', '⚠️ Email submission may have failed - no password field, no error', 'warn', 'yellow') log(false, 'CREATOR', 'Waiting longer for response...', 'log', 'cyan') await this.humanDelay(5000, 7000) } } else { log(false, 'CREATOR', `✅ URL changed: ${urlBeforeClick} → ${urlAfterClick}`, 'log', 'green') } const result = await this.handleEmailErrors(email) if (!result.success) { return null } // CRITICAL: If email was accepted by handleEmailErrors, trust that result // Don't do additional error check here as it may detect false positives // (e.g., transient errors that were already handled) log(false, 'CREATOR', `✅ Email step completed successfully: ${result.email}`, 'log', 'green') return result.email } private async handleEmailErrors(originalEmail: string): Promise<{ success: boolean; email: string | null }> { await this.humanDelay(1000, 1500) const errorLocator = this.page.locator('div[id*="Error"], div[role="alert"]').first() const errorVisible = await errorLocator.isVisible().catch(() => false) if (!errorVisible) { log(false, 'CREATOR', `✅ Email accepted: ${originalEmail}`, 'log', 'green') return { success: true, email: originalEmail } } const errorText = await errorLocator.textContent().catch(() => '') || '' log(false, 'CREATOR', `Email error: ${errorText}`, 'warn', 'yellow') // Check for reserved domain error if (errorText && (errorText.toLowerCase().includes('reserved') || errorText.toLowerCase().includes('réservé'))) { return await this.handleReservedDomain(originalEmail) } // Check for email taken error if (errorText && (errorText.toLowerCase().includes('taken') || errorText.toLowerCase().includes('pris') || errorText.toLowerCase().includes('already') || errorText.toLowerCase().includes('déjà'))) { return await this.handleEmailTaken() } log(false, 'CREATOR', 'Unknown error type, pausing for inspection', 'error') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') await new Promise(() => {}) return { success: false, email: null } } private async handleReservedDomain(originalEmail: string): Promise<{ success: boolean; email: string | null }> { log(false, 'CREATOR', `Domain blocked: ${originalEmail.split('@')[1]}`, 'warn', 'yellow') const username = originalEmail.split('@')[0] const newEmail = `${username}@outlook.com` log(false, 'CREATOR', `Retrying with: ${newEmail}`, 'log', 'cyan') const emailInput = this.page.locator('input[type="email"]').first() // CRITICAL: Retry fill with SMART verification (handles domain separation) const retryFillSuccess = await this.retryOperation( async () => { await emailInput.clear() await this.humanDelay(800, 1500) await emailInput.fill(newEmail) await this.humanDelay(1200, 2500) // SMART VERIFICATION: Microsoft may separate domain const inputValue = await emailInput.inputValue().catch(() => '') const emailUsername = newEmail.split('@')[0] const emailDomain = newEmail.split('@')[1] if (inputValue === newEmail || (inputValue === emailUsername && (emailDomain === 'outlook.com' || emailDomain === 'hotmail.com' || emailDomain === 'outlook.fr'))) { log(false, 'CREATOR', '[EMAIL_RETRY] ✅ Value verified (domain may be separated)', 'log', 'green') return true } else { log(false, 'CREATOR', `[EMAIL_RETRY] ⚠️ Unexpected value: "${inputValue}"`, 'warn', 'yellow') throw new Error('Email retry input value not verified') } }, 'EMAIL_RETRY_FILL', 3, 1000 ) if (!retryFillSuccess) { log(false, 'CREATOR', 'Failed to fill retry email', 'error') return { success: false, email: null } } const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() await nextBtn.click() await this.humanDelay(2000, 3000) await this.waitForPageStable('RETRY_EMAIL', 15000) return await this.handleEmailErrors(newEmail) } private async handleEmailTaken(): Promise<{ success: boolean; email: string | null }> { log(false, 'CREATOR', 'Email taken, looking for Microsoft suggestions...', 'log', 'yellow') await this.humanDelay(2000, 3000) await this.waitForPageStable('EMAIL_SUGGESTIONS', 10000) // Multiple selectors for suggestions container const suggestionSelectors = [ 'div[data-testid="suggestions"]', 'div[role="toolbar"]', 'div.fui-TagGroup', 'div[class*="suggestions"]' ] let suggestionsContainer = null for (const selector of suggestionSelectors) { const container = this.page.locator(selector).first() const visible = await container.isVisible().catch(() => false) if (visible) { suggestionsContainer = container log(false, 'CREATOR', `Found suggestions with selector: ${selector}`, 'log', 'green') break } } if (!suggestionsContainer) { log(false, 'CREATOR', 'No suggestions found', 'warn', 'yellow') // Debug: check HTML content const pageContent = await this.page.content() const hasDataTestId = pageContent.includes('data-testid="suggestions"') const hasToolbar = pageContent.includes('role="toolbar"') log(false, 'CREATOR', `Debug - suggestions in HTML: ${hasDataTestId}, toolbar: ${hasToolbar}`, 'warn', 'yellow') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') await new Promise(() => {}) return { success: false, email: null } } // Find all suggestion buttons const suggestionButtons = await suggestionsContainer.locator('button').all() log(false, 'CREATOR', `Found ${suggestionButtons.length} suggestion buttons`, 'log', 'cyan') if (suggestionButtons.length === 0) { log(false, 'CREATOR', 'Suggestions container found but no buttons inside', 'error') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') await new Promise(() => {}) return { success: false, email: null } } // Get text from first suggestion before clicking const firstButton = suggestionButtons[0] if (!firstButton) { log(false, 'CREATOR', 'First button is undefined', 'error') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') await new Promise(() => {}) return { success: false, email: null } } const suggestedEmail = await firstButton.textContent().catch(() => '') || '' let cleanEmail = suggestedEmail.trim() // If suggestion doesn't have @domain, it's just the username - add @outlook.com if (cleanEmail && !cleanEmail.includes('@')) { cleanEmail = `${cleanEmail}@outlook.com` log(false, 'CREATOR', `Suggestion is username only, adding domain: ${cleanEmail}`, 'log', 'cyan') } if (!cleanEmail) { log(false, 'CREATOR', 'Could not extract email from suggestion button', 'error') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') await new Promise(() => {}) return { success: false, email: null } } log(false, 'CREATOR', `Selecting suggestion: ${cleanEmail}`, 'log', 'cyan') // Click the suggestion await firstButton.click() await this.humanDelay(1500, 2500) // Verify the email input was updated const emailInput = this.page.locator('input[type="email"]').first() const inputValue = await emailInput.inputValue().catch(() => '') if (inputValue) { log(false, 'CREATOR', `✅ Suggestion applied: ${inputValue}`, 'log', 'green') } // Check if error is gone const errorLocator = this.page.locator('div[id*="Error"], div[role="alert"]').first() const errorStillVisible = await errorLocator.isVisible().catch(() => false) if (errorStillVisible) { log(false, 'CREATOR', 'Error still visible after clicking suggestion', 'warn', 'yellow') // Try clicking Next to submit const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() const nextEnabled = await nextBtn.isEnabled().catch(() => false) if (nextEnabled) { log(false, 'CREATOR', 'Clicking Next to submit suggestion', 'log') await nextBtn.click() await this.humanDelay(2000, 3000) // Final check const finalError = await errorLocator.isVisible().catch(() => false) if (finalError) { log(false, 'CREATOR', 'Failed to resolve error', 'error') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') await new Promise(() => {}) return { success: false, email: null } } } } else { // Error is gone, click Next to continue log(false, 'CREATOR', 'Suggestion accepted, clicking Next', 'log', 'green') const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() await nextBtn.click() await this.humanDelay(2000, 3000) } log(false, 'CREATOR', `✅ Using suggested email: ${cleanEmail}`, 'log', 'green') return { success: true, email: cleanEmail } } private async clickNext(step: string): Promise { log(false, 'CREATOR', `Clicking Next button (${step})...`, 'log') // CRITICAL: Ensure page is stable before clicking await this.waitForPageStable(`BEFORE_NEXT_${step.toUpperCase()}`, 15000) // Find button by test id or type submit const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() // CRITICAL: Verify button is ready const isReady = await this.verifyElementReady( 'button[data-testid="primaryButton"], button[type="submit"]', `NEXT_BUTTON_${step.toUpperCase()}`, 15000 ) if (!isReady) { log(false, 'CREATOR', 'Next button not ready, waiting longer...', 'warn', 'yellow') await this.humanDelay(3000, 5000) } // Ensure button is enabled const isEnabled = await nextBtn.isEnabled() if (!isEnabled) { log(false, 'CREATOR', 'Waiting for Next button to be enabled...', 'warn') await this.humanDelay(3000, 5000) } // Get current URL and page state before clicking const urlBefore = this.page.url() await nextBtn.click() log(false, 'CREATOR', `✅ Clicked Next (${step})`, 'log', 'green') // CRITICAL: Wait for page to process the click await this.humanDelay(3000, 5000) // CRITICAL: Wait for page to be stable after clicking await this.waitForPageStable(`AFTER_NEXT_${step.toUpperCase()}`, 20000) // CRITICAL: Verify the click was successful const urlAfter = this.page.url() let clickSuccessful = false if (urlBefore !== urlAfter) { log(false, 'CREATOR', `✅ Navigation detected: ${urlBefore} → ${urlAfter}`, 'log', 'green') clickSuccessful = true } else { log(false, 'CREATOR', `URL unchanged after clicking Next (${step})`, 'log', 'yellow') // URL didn't change - this might be OK if content changed // Wait a bit more and check for errors await this.humanDelay(2000, 3000) const hasErrors = !(await this.verifyNoErrors(`NEXT_${step.toUpperCase()}`)) if (hasErrors) { log(false, 'CREATOR', `❌ Errors detected after clicking Next (${step})`, 'error') return false } // No errors - assume success (some pages don't change URL) log(false, 'CREATOR', `No errors detected, assuming Next (${step}) was successful`, 'log', 'yellow') clickSuccessful = true } return clickSuccessful } private async fillPassword(): Promise { log(false, 'CREATOR', 'Waiting for password page...', 'log') await this.page.locator('h1[data-testid="title"]').first().waitFor({ timeout: 20000 }) await this.waitForPageStable('PASSWORD_PAGE', 15000) await this.humanDelay(1000, 2000) log(false, 'CREATOR', 'Generating strong password...', 'log') const password = this.dataGenerator.generatePassword() const passwordInput = this.page.locator('input[type="password"]').first() await passwordInput.waitFor({ timeout: 15000 }) // CRITICAL: Retry fill with verification const passwordFillSuccess = await this.retryOperation( async () => { await passwordInput.clear() await this.humanDelay(800, 1500) // INCREASED from 500-1000 await passwordInput.fill(password) await this.humanDelay(1200, 2500) // INCREASED from 800-2000 // Verify value was filled correctly const verified = await this.verifyInputValue('input[type="password"]', password, 'PASSWORD_INPUT') if (!verified) { throw new Error('Password input value not verified') } return true }, 'PASSWORD_FILL', 3, 1000 ) if (!passwordFillSuccess) { log(false, 'CREATOR', 'Failed to fill password after retries', 'error') return null } log(false, 'CREATOR', '✅ Password filled (hidden for security)', 'log', 'green') return password } private async extractEmail(): Promise { log(false, 'CREATOR', 'Extracting email from identity badge...', 'log') // Multiple selectors for identity badge (language-independent) const badgeSelectors = [ '#bannerText', 'div[id="identityBadge"] div', 'div[data-testid="identityBanner"] div', 'div[class*="identityBanner"]', 'span[class*="identityText"]' ] for (const selector of badgeSelectors) { try { const badge = this.page.locator(selector).first() await badge.waitFor({ timeout: 5000 }) const email = await badge.textContent() if (email && email.includes('@')) { const cleanEmail = email.trim() log(false, 'CREATOR', `✅ Email extracted: ${cleanEmail}`, 'log', 'green') return cleanEmail } } catch { // Try next selector continue } } log(false, 'CREATOR', 'Could not find identity badge (not critical)', 'warn') return null } private async fillBirthdate(): Promise<{ day: number; month: number; year: number } | null> { log(false, 'CREATOR', 'Filling birthdate...', 'log') await this.waitForPageStable('BIRTHDATE_PAGE', 15000) const birthdate = this.dataGenerator.generateBirthdate() try { await this.humanDelay(2000, 3000) // === DAY DROPDOWN === const dayButton = this.page.locator('button[name="BirthDay"], button#BirthDayDropdown').first() await dayButton.waitFor({ timeout: 15000, state: 'visible' }) log(false, 'CREATOR', 'Clicking day dropdown...', 'log') // CRITICAL: Retry click if it fails const dayClickSuccess = await this.retryOperation( async () => { await dayButton.click({ force: true }) await this.humanDelay(1500, 2500) // INCREASED delay // Verify dropdown opened const dayOptionsContainer = this.page.locator('div[role="listbox"], ul[role="listbox"]').first() const isOpen = await dayOptionsContainer.isVisible().catch(() => false) if (!isOpen) { throw new Error('Day dropdown did not open') } return true }, 'DAY_DROPDOWN_OPEN', 3, 1000 ) if (!dayClickSuccess) { log(false, 'CREATOR', 'Failed to open day dropdown after retries', 'error') return null } log(false, 'CREATOR', '✅ Day dropdown opened', 'log', 'green') // Select day from dropdown log(false, 'CREATOR', `Selecting day: ${birthdate.day}`, 'log') const dayOption = this.page.locator(`div[role="option"]:has-text("${birthdate.day}"), li[role="option"]:has-text("${birthdate.day}")`).first() await dayOption.waitFor({ timeout: 5000, state: 'visible' }) await dayOption.click() await this.humanDelay(1500, 2500) // INCREASED delay // CRITICAL: Wait for dropdown to FULLY close await this.waitForDropdownClosed('DAY_DROPDOWN', 8000) await this.humanDelay(2000, 3000) // INCREASED safety delay // === MONTH DROPDOWN === const monthButton = this.page.locator('button[name="BirthMonth"], button#BirthMonthDropdown').first() await monthButton.waitFor({ timeout: 10000, state: 'visible' }) log(false, 'CREATOR', 'Clicking month dropdown...', 'log') // CRITICAL: Retry click if it fails const monthClickSuccess = await this.retryOperation( async () => { await monthButton.click({ force: true }) await this.humanDelay(1500, 2500) // INCREASED delay // Verify dropdown opened const monthOptionsContainer = this.page.locator('div[role="listbox"], ul[role="listbox"]').first() const isOpen = await monthOptionsContainer.isVisible().catch(() => false) if (!isOpen) { throw new Error('Month dropdown did not open') } return true }, 'MONTH_DROPDOWN_OPEN', 3, 1000 ) if (!monthClickSuccess) { log(false, 'CREATOR', 'Failed to open month dropdown after retries', 'error') return null } log(false, 'CREATOR', '✅ Month dropdown opened', 'log', 'green') // Select month by data-value attribute or by position log(false, 'CREATOR', `Selecting month: ${birthdate.month}`, 'log') const monthOption = this.page.locator(`div[role="option"][data-value="${birthdate.month}"], li[role="option"][data-value="${birthdate.month}"]`).first() // Fallback: if data-value doesn't work, try by index const monthVisible = await monthOption.isVisible().catch(() => false) if (monthVisible) { await monthOption.click() log(false, 'CREATOR', '✅ Month selected by data-value', 'log', 'green') } else { log(false, 'CREATOR', `Fallback: selecting month by nth-child(${birthdate.month})`, 'warn', 'yellow') const monthOptionByIndex = this.page.locator(`div[role="option"]:nth-child(${birthdate.month}), li[role="option"]:nth-child(${birthdate.month})`).first() await monthOptionByIndex.click() } await this.humanDelay(1500, 2500) // INCREASED delay // CRITICAL: Wait for dropdown to FULLY close await this.waitForDropdownClosed('MONTH_DROPDOWN', 8000) await this.humanDelay(2000, 3000) // INCREASED safety delay // === YEAR INPUT === const yearInput = this.page.locator('input[name="BirthYear"], input[type="number"]').first() await yearInput.waitFor({ timeout: 10000, state: 'visible' }) log(false, 'CREATOR', `Filling year: ${birthdate.year}`, 'log') // CRITICAL: Retry fill with verification const yearFillSuccess = await this.retryOperation( async () => { await yearInput.clear() await this.humanDelay(500, 1000) await yearInput.fill(birthdate.year.toString()) await this.humanDelay(1000, 2000) // Verify value was filled correctly const verified = await this.verifyInputValue( 'input[name="BirthYear"], input[type="number"]', birthdate.year.toString(), 'YEAR_INPUT' ) if (!verified) { throw new Error('Year input value not verified') } return true }, 'YEAR_FILL', 3, 1000 ) if (!yearFillSuccess) { log(false, 'CREATOR', 'Failed to fill year after retries', 'error') return null } log(false, 'CREATOR', `✅ Birthdate filled: ${birthdate.day}/${birthdate.month}/${birthdate.year}`, 'log', 'green') // CRITICAL: Verify no errors appeared after filling birthdate const noErrors = await this.verifyNoErrors('BIRTHDATE_VERIFICATION') if (!noErrors) { log(false, 'CREATOR', '❌ Errors detected after filling birthdate', 'error') return null } // CRITICAL: Verify Next button is enabled (indicates form is valid) await this.humanDelay(1000, 2000) const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() const nextEnabled = await nextBtn.isEnabled().catch(() => false) if (!nextEnabled) { log(false, 'CREATOR', '⚠️ Next button not enabled after filling birthdate', 'warn', 'yellow') log(false, 'CREATOR', 'Waiting for form validation...', 'log', 'cyan') await this.humanDelay(3000, 5000) const retryEnabled = await nextBtn.isEnabled().catch(() => false) if (!retryEnabled) { log(false, 'CREATOR', '❌ Next button still disabled - form may be invalid', 'error') return null } } log(false, 'CREATOR', '✅ Birthdate form validated successfully', 'log', 'green') // CRITICAL: Extra safety delay before submitting await this.humanDelay(2000, 3000) return birthdate } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `Error filling birthdate: ${msg}`, 'error') return null } } private async fillNames(email: string): Promise<{ firstName: string; lastName: string } | null> { log(false, 'CREATOR', 'Filling first and last name...', 'log') await this.waitForPageStable('NAMES_PAGE', 15000) const names = this.dataGenerator.generateNames(email) try { await this.humanDelay(1000, 2000) const firstNameSelectors = [ 'input[id*="firstName"]', 'input[name*="firstName"]', 'input[id*="first"]', 'input[name*="first"]', 'input[aria-label*="First"]', 'input[placeholder*="First"]' ] let firstNameInput = null for (const selector of firstNameSelectors) { const input = this.page.locator(selector).first() const visible = await input.isVisible().catch(() => false) if (visible) { firstNameInput = input break } } if (!firstNameInput) { log(false, 'CREATOR', 'Could not find first name input', 'error') return null } // CRITICAL: Retry fill with verification const firstNameFillSuccess = await this.retryOperation( async () => { await firstNameInput.clear() await this.humanDelay(800, 1500) // INCREASED from 500-1000 await firstNameInput.fill(names.firstName) await this.humanDelay(1200, 2500) // INCREASED from 800-2000 return true }, 'FIRSTNAME_FILL', 3, 1000 ) if (!firstNameFillSuccess) { log(false, 'CREATOR', 'Failed to fill first name after retries', 'error') return null } // Fill last name with multiple selector fallbacks const lastNameSelectors = [ 'input[id*="lastName"]', 'input[name*="lastName"]', 'input[id*="last"]', 'input[name*="last"]', 'input[aria-label*="Last"]', 'input[placeholder*="Last"]' ] let lastNameInput = null for (const selector of lastNameSelectors) { const input = this.page.locator(selector).first() const visible = await input.isVisible().catch(() => false) if (visible) { lastNameInput = input break } } if (!lastNameInput) { log(false, 'CREATOR', 'Could not find last name input', 'error') return null } // CRITICAL: Retry fill with verification const lastNameFillSuccess = await this.retryOperation( async () => { await lastNameInput.clear() await this.humanDelay(800, 1500) // INCREASED from 500-1000 await lastNameInput.fill(names.lastName) await this.humanDelay(1200, 2500) // INCREASED from 800-2000 return true }, 'LASTNAME_FILL', 3, 1000 ) if (!lastNameFillSuccess) { log(false, 'CREATOR', 'Failed to fill last name after retries', 'error') return null } log(false, 'CREATOR', `✅ Names filled: ${names.firstName} ${names.lastName}`, 'log', 'green') // CRITICAL: Verify no errors appeared after filling names const noErrors = await this.verifyNoErrors('NAMES_VERIFICATION') if (!noErrors) { log(false, 'CREATOR', '❌ Errors detected after filling names', 'error') return null } // CRITICAL: Verify Next button is enabled (indicates form is valid) await this.humanDelay(1000, 2000) const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() const nextEnabled = await nextBtn.isEnabled().catch(() => false) if (!nextEnabled) { log(false, 'CREATOR', '⚠️ Next button not enabled after filling names', 'warn', 'yellow') log(false, 'CREATOR', 'Waiting for form validation...', 'log', 'cyan') await this.humanDelay(3000, 5000) const retryEnabled = await nextBtn.isEnabled().catch(() => false) if (!retryEnabled) { log(false, 'CREATOR', '❌ Next button still disabled - form may be invalid', 'error') return null } } log(false, 'CREATOR', '✅ Names form validated successfully', 'log', 'green') return names } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `Error filling names: ${msg}`, 'error') return null } } private async waitForCaptcha(): Promise { try { // Wait a bit to let the page load await this.humanDelay(1500, 2500) // Check for CAPTCHA iframe (most reliable indicator) const captchaIframe = this.page.locator('iframe[data-testid="humanCaptchaIframe"]').first() const iframeVisible = await captchaIframe.isVisible().catch(() => false) if (iframeVisible) { log(false, 'CREATOR', 'CAPTCHA detected via iframe', 'warn', 'yellow') return true } // Check multiple indicators for CAPTCHA const captchaIndicators = [ 'h1[data-testid="title"]', 'div[id*="captcha"]', 'div[class*="captcha"]', 'div[id*="enforcement"]', 'img[data-testid="accessibleImg"]' ] for (const selector of captchaIndicators) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { const text = await element.textContent().catch(() => '') // Check for CAPTCHA-related keywords if (text && ( text.toLowerCase().includes('vérif') || text.toLowerCase().includes('verify') || text.toLowerCase().includes('human') || text.toLowerCase().includes('humain') || text.toLowerCase().includes('puzzle') || text.toLowerCase().includes('captcha') || text.toLowerCase().includes('prove you') )) { log(false, 'CREATOR', `CAPTCHA detected with text: ${text.substring(0, 50)}`, 'warn', 'yellow') return true } } } return false } catch { return false } } private async waitForCaptchaSolved(): Promise { const maxWaitTime = 10 * 60 * 1000 // 10 minutes const startTime = Date.now() let lastLogTime = startTime while (Date.now() - startTime < maxWaitTime) { try { // Log progress every 30 seconds if (Date.now() - lastLogTime > 30000) { const elapsed = Math.floor((Date.now() - startTime) / 1000) log(false, 'CREATOR', `Still waiting for CAPTCHA... (${elapsed}s elapsed)`, 'log', 'yellow') lastLogTime = Date.now() } // Check if CAPTCHA is still present const captchaStillPresent = await this.waitForCaptcha() if (!captchaStillPresent) { // CAPTCHA solved! But account creation may still be in progress log(false, 'CREATOR', '✅ CAPTCHA solved! Waiting for account creation...', 'log', 'green') // CRITICAL: Wait MUCH longer for Microsoft to process log(false, 'CREATOR', 'Giving Microsoft extra time to process (15-20s)...', 'log', 'cyan') await this.humanDelay(15000, 20000) // INCREASED from 3-5s // CRITICAL: Wait for Microsoft to finish creating the account await this.waitForAccountCreation() // CRITICAL: Extra delay after account creation log(false, 'CREATOR', 'Account creation complete, stabilizing (10-15s)...', 'log', 'cyan') await this.humanDelay(10000, 15000) return } // Wait before checking again await this.page.waitForTimeout(2000) } catch { // Error checking, assume CAPTCHA is solved return } } throw new Error('CAPTCHA solving timeout - waited 10 minutes') } private async handlePostCreationQuestions(): Promise { log(false, 'CREATOR', 'Handling post-creation questions...', 'log', 'cyan') // CRITICAL: Ensure page is fully loaded and stable before proceeding log(false, 'CREATOR', 'Waiting for page to stabilize after account creation...', 'log', 'cyan') await this.waitForPageStable('POST_CREATION', 40000) // INCREASED timeout // CRITICAL: Additional LONG safety delay log(false, 'CREATOR', 'Extra stabilization delay (10-15s)...', 'log', 'cyan') await this.humanDelay(10000, 15000) // INCREASED from 3-5s // CRITICAL: Handle passkey prompt - MUST REFUSE await this.handlePasskeyPrompt() // Additional delay between prompts await this.humanDelay(3000, 5000) // Handle "Stay signed in?" (KMSI) prompt const kmsiSelectors = [ '[data-testid="kmsiVideo"]', 'div:has-text("Stay signed in?")', 'div:has-text("Rester connecté")', 'button[data-testid="primaryButton"]' ] for (let i = 0; i < 3; i++) { let found = false for (const selector of kmsiSelectors) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', 'Stay signed in prompt detected', 'log', 'yellow') // Click "Yes" button const yesButton = this.page.locator('button[data-testid="primaryButton"]').first() const yesVisible = await yesButton.isVisible().catch(() => false) if (yesVisible) { await yesButton.click() await this.humanDelay(2000, 3000) await this.waitForPageStable('AFTER_KMSI', 15000) log(false, 'CREATOR', '✅ Accepted "Stay signed in"', 'log', 'green') found = true break } } } if (!found) break await this.humanDelay(1000, 2000) } // Handle any other prompts (biometric, etc.) const genericPrompts = [ '[data-testid="biometricVideo"]', 'button[id*="close"]', 'button[aria-label*="Close"]' ] for (const selector of genericPrompts) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `Closing prompt: ${selector}`, 'log', 'yellow') // Try to close it const closeButton = this.page.locator('button[data-testid="secondaryButton"], button[id*="close"]').first() const closeVisible = await closeButton.isVisible().catch(() => false) if (closeVisible) { await closeButton.click() await this.humanDelay(1500, 2500) log(false, 'CREATOR', '✅ Closed prompt', 'log', 'green') } } } log(false, 'CREATOR', '✅ Post-creation questions handled', 'log', 'green') } private async handlePasskeyPrompt(): Promise { log(false, 'CREATOR', 'Checking for passkey setup prompt...', 'log', 'cyan') // CRITICAL: Wait MUCH longer for passkey prompt to appear // Microsoft may show this after several seconds log(false, 'CREATOR', 'Waiting for potential passkey prompt (8-12s)...', 'log', 'cyan') await this.humanDelay(8000, 12000) // INCREASED from 5-7s // Ensure page is stable before checking await this.waitForPageStable('PASSKEY_CHECK', 15000) // Multiple selectors for passkey prompt detection const passkeyDetectionSelectors = [ 'div:has-text("passkey")', 'div:has-text("clé d\'accès")', 'div:has-text("Set up a passkey")', 'div:has-text("Configurer une clé")', '[data-testid*="passkey"]', 'button:has-text("Skip")', 'button:has-text("Not now")', 'button:has-text("Ignorer")', 'button:has-text("Plus tard")' ] let passkeyPromptFound = false for (const selector of passkeyDetectionSelectors) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { passkeyPromptFound = true log(false, 'CREATOR', '⚠️ Passkey setup prompt detected - REFUSING', 'warn', 'yellow') break } } if (!passkeyPromptFound) { log(false, 'CREATOR', 'No passkey prompt detected', 'log', 'green') return } // Try to click refuse/skip buttons const refuseButtonSelectors = [ 'button:has-text("Skip")', 'button:has-text("Not now")', 'button:has-text("No")', 'button:has-text("Cancel")', 'button:has-text("Ignorer")', 'button:has-text("Plus tard")', 'button:has-text("Non")', 'button:has-text("Annuler")', 'button[data-testid="secondaryButton"]', 'button[id*="cancel"]', 'button[id*="skip"]' ] for (const selector of refuseButtonSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `Clicking refuse button: ${selector}`, 'log', 'cyan') await button.click() await this.humanDelay(2000, 3000) log(false, 'CREATOR', '✅ Passkey setup REFUSED', 'log', 'green') return } } log(false, 'CREATOR', '⚠️ Could not find refuse button for passkey prompt', 'warn', 'yellow') } private async verifyAccountActive(): Promise { log(false, 'CREATOR', 'Verifying account is active...', 'log', 'cyan') // CRITICAL: Ensure current page is stable before navigating await this.waitForPageStable('PRE_VERIFICATION', 20000) // INCREASED // CRITICAL: MUCH longer delay before navigation log(false, 'CREATOR', 'Extra delay before navigation (8-12s)...', 'log', 'cyan') await this.humanDelay(8000, 12000) // INCREASED from 3-5s // Navigate to Bing Rewards try { log(false, 'CREATOR', 'Navigating to rewards.bing.com...', 'log', 'cyan') await this.page.goto('https://rewards.bing.com/', { waitUntil: 'networkidle', timeout: 60000 }) // CRITICAL: Wait for page to be fully stable after navigation await this.waitForPageStable('REWARDS_PAGE', 40000) // INCREASED // CRITICAL: MUCH longer safety delay for identity to load log(false, 'CREATOR', 'Waiting for user identity to fully load (10-15s)...', 'log', 'cyan') await this.humanDelay(10000, 15000) // INCREASED from 5-7s const currentUrl = this.page.url() log(false, 'CREATOR', `Current URL: ${currentUrl}`, 'log', 'cyan') // CRITICAL: Verify we're actually on rewards page and logged in if (!currentUrl.includes('rewards.bing.com')) { if (currentUrl.includes('login.live.com')) { log(false, 'CREATOR', '⚠️ Still on login page - account may not be fully activated', 'warn', 'yellow') // CRITICAL: Wait MUCH longer and retry log(false, 'CREATOR', 'Waiting longer before retry (15-20s)...', 'log', 'cyan') await this.humanDelay(15000, 20000) // INCREASED await this.page.goto('https://rewards.bing.com/', { waitUntil: 'networkidle', timeout: 60000 }) await this.waitForPageStable('REWARDS_RETRY', 40000) await this.humanDelay(10000, 15000) // Additional delay after retry } else { log(false, 'CREATOR', `⚠️ Unexpected URL: ${currentUrl}`, 'warn', 'yellow') } } log(false, 'CREATOR', '✅ Successfully navigated to rewards.bing.com', 'log', 'green') // CRITICAL: Wait LONGER for user identity to load before declaring success log(false, 'CREATOR', 'Final wait for complete page load (8-12s)...', 'log', 'cyan') await this.humanDelay(8000, 12000) // INCREASED from 5-7s // CRITICAL: Verify user identity is loaded log(false, 'CREATOR', 'Verifying user identity...', 'log', 'cyan') const identitySelectors = [ '[data-bi-id="userIdentity"]', '[id*="user"]', 'button[aria-label*="Account"]', '#id_n', // User dropdown '.mee_header_profile' // Profile area ] let identityVerified = false for (const selector of identitySelectors) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { const text = await element.textContent().catch(() => '') if (text && text.trim().length > 0) { log(false, 'CREATOR', `✅ Verified identity: ${text.substring(0, 50)}`, 'log', 'green') identityVerified = true break } } } if (!identityVerified) { log(false, 'CREATOR', '⚠️ Could not verify user identity on page', 'warn', 'yellow') // CRITICAL: Wait MUCH longer and check again log(false, 'CREATOR', 'Waiting longer for identity (10-15s)...', 'log', 'cyan') await this.humanDelay(10000, 15000) // INCREASED from 5-7s for (const selector of identitySelectors) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', '✅ Identity verified on retry', 'log', 'green') identityVerified = true break } } } if (identityVerified) { log(false, 'CREATOR', '✅ Account is active and logged in!', 'log', 'green') } else { log(false, 'CREATOR', '⚠️ Account state uncertain - proceeding anyway', 'warn', 'yellow') } // NOW handle popups and tour - AFTER confirming we're logged in log(false, 'CREATOR', 'Preparing to handle welcome tour and popups...', 'log', 'cyan') await this.humanDelay(5000, 8000) // INCREASED from 3-5s await this.handleRewardsWelcomeTour() log(false, 'CREATOR', 'Pausing between tour and popups...', 'log', 'cyan') await this.humanDelay(5000, 8000) // INCREASED from 3-5s await this.handleRewardsPopups() // If we have a referral URL, ensure we join via it if (this.referralUrl) { log(false, 'CREATOR', 'Preparing for referral enrollment...', 'log', 'cyan') await this.humanDelay(5000, 7000) // INCREASED from 3-4s await this.ensureRewardsEnrollment() } } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `Warning: Could not verify account: ${msg}`, 'warn', 'yellow') } } private async handleRewardsWelcomeTour(): Promise { log(false, 'CREATOR', 'Checking for Microsoft Rewards welcome tour...', 'log', 'cyan') // CRITICAL: Ensure page is stable before checking for tour await this.waitForPageStable('WELCOME_TOUR', 30000) // INCREASED // CRITICAL: MUCH longer delay for tour to appear log(false, 'CREATOR', 'Waiting for welcome tour to appear (8-12s)...', 'log', 'cyan') await this.humanDelay(8000, 12000) // INCREASED from 5-7s // Try to handle the welcome tour (multiple Next buttons) const maxClicks = 5 for (let i = 0; i < maxClicks; i++) { // Check for welcome tour indicators const welcomeIndicators = [ 'img[src*="Get%20cool%20prizes"]', 'img[alt*="Welcome to Microsoft Rewards"]', 'div.welcome-tour', 'a#fre-next-button', 'a.welcome-tour-button.next-button', 'a.next-button.c-call-to-action' ] let tourFound = false for (const selector of welcomeIndicators) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { tourFound = true log(false, 'CREATOR', `Welcome tour detected (step ${i + 1})`, 'log', 'yellow') break } } if (!tourFound) { log(false, 'CREATOR', 'No more welcome tour steps', 'log', 'green') break } // Try to click Next button const nextButtonSelectors = [ 'a#fre-next-button', 'a.welcome-tour-button.next-button', 'a.next-button.c-call-to-action', 'button:has-text("Next")', 'a:has-text("Next")', 'button:has-text("Suivant")', 'a:has-text("Suivant")' ] let clickedNext = false for (const selector of nextButtonSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `Clicking Next button: ${selector}`, 'log', 'cyan') await button.click() await this.humanDelay(4000, 6000) // INCREASED from 3-4s await this.waitForPageStable('AFTER_TOUR_NEXT', 20000) // INCREASED clickedNext = true log(false, 'CREATOR', `✅ Clicked Next (step ${i + 1})`, 'log', 'green') break } } if (!clickedNext) { const pinButtonSelectors = [ 'a#claim-button', 'a:has-text("Pin and start earning")', 'a:has-text("Épingler et commencer")', 'a.welcome-tour-button[href*="pin=true"]' ] for (const selector of pinButtonSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', 'Clicking "Pin and start earning" button', 'log', 'cyan') await button.click() await this.humanDelay(3000, 4000) await this.waitForPageStable('AFTER_PIN', 15000) log(false, 'CREATOR', '✅ Clicked Pin button', 'log', 'green') break } } break } // Wait between steps to avoid spamming await this.humanDelay(2000, 3000) } log(false, 'CREATOR', '✅ Welcome tour handled', 'log', 'green') } private async handleRewardsPopups(): Promise { log(false, 'CREATOR', 'Checking for Microsoft Rewards popups...', 'log', 'cyan') // CRITICAL: Ensure page is stable before checking for popups await this.waitForPageStable('REWARDS_POPUPS', 30000) // INCREASED // CRITICAL: Wait MUCH longer for any popups to appear log(false, 'CREATOR', 'Waiting for popups to appear (8-12s)...', 'log', 'cyan') await this.humanDelay(8000, 12000) // INCREASED from 5-7s // Handle ReferAndEarn popup const referralPopupSelectors = [ 'img[src*="ReferAndEarnPopUpImgUpdated"]', 'div.dashboardPopUp', 'a.dashboardPopUpPopUpSelectButton', 'a#reward_pivot_earn' ] let referralPopupFound = false for (const selector of referralPopupSelectors) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { referralPopupFound = true log(false, 'CREATOR', 'Referral popup detected', 'log', 'yellow') break } } if (referralPopupFound) { // CRITICAL: Wait longer before clicking to ensure popup is fully loaded log(false, 'CREATOR', 'Referral popup found, waiting for it to stabilize (3-5s)...', 'log', 'cyan') await this.humanDelay(3000, 5000) // INCREASED from 2-3s // Click "Get started" button const getStartedSelectors = [ 'a.dashboardPopUpPopUpSelectButton', 'a#reward_pivot_earn', 'a:has-text("Get started")', 'a:has-text("Commencer")', 'button:has-text("Get started")', 'button:has-text("Commencer")' ] for (const selector of getStartedSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', 'Clicking "Get started" button', 'log', 'cyan') await button.click() await this.humanDelay(4000, 6000) // INCREASED from 3-4s await this.waitForPageStable('AFTER_GET_STARTED', 20000) // INCREASED log(false, 'CREATOR', '✅ Clicked Get started', 'log', 'green') break } } } const genericCloseSelectors = [ 'button[aria-label*="Close"]', 'button[aria-label*="Fermer"]', 'button.close', 'a.close' ] for (const selector of genericCloseSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `Closing popup with selector: ${selector}`, 'log', 'cyan') await button.click() await this.humanDelay(2000, 3000) await this.waitForPageStable('AFTER_CLOSE_POPUP', 10000) } } log(false, 'CREATOR', '✅ Popups handled', 'log', 'green') } private async ensureRewardsEnrollment(): Promise { log(false, 'CREATOR', 'Ensuring Microsoft Rewards enrollment via referral URL...', 'log', 'cyan') if (!this.referralUrl) { log(false, 'CREATOR', 'No referral URL provided, skipping enrollment check', 'warn', 'yellow') return } try { // Navigate to referral URL log(false, 'CREATOR', `Navigating to referral URL: ${this.referralUrl}`, 'log', 'cyan') await this.page.goto(this.referralUrl, { waitUntil: 'networkidle', timeout: 60000 }) // CRITICAL: Wait for page to be stable after navigation await this.waitForPageStable('REFERRAL_ENROLLMENT', 40000) // INCREASED // CRITICAL: Longer additional delay log(false, 'CREATOR', 'Stabilizing after referral navigation (8-12s)...', 'log', 'cyan') await this.humanDelay(8000, 12000) // INCREASED from 5-7s // Look for "Join Microsoft Rewards" button const joinButtonSelectors = [ 'a#start-earning-rewards-link', 'a.cta.learn-more-btn', 'a[href*="createNewUser"]', 'a:has-text("Join Microsoft Rewards")', 'a:has-text("Rejoindre Microsoft Rewards")', 'button:has-text("Join")', 'button:has-text("Rejoindre")' ] let joined = false for (const selector of joinButtonSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `Clicking "Join Microsoft Rewards" button: ${selector}`, 'log', 'cyan') await button.click() await this.humanDelay(5000, 8000) // INCREASED from 3-5s await this.waitForPageStable('AFTER_JOIN', 30000) // INCREASED log(false, 'CREATOR', '✅ Clicked Join button', 'log', 'green') joined = true break } } if (!joined) { log(false, 'CREATOR', 'Join button not found - account may already be enrolled', 'log', 'yellow') } // CRITICAL: Wait MUCH longer for enrollment to complete and page to stabilize log(false, 'CREATOR', 'Waiting for enrollment to complete...', 'log', 'cyan') await this.waitForPageStable('POST_ENROLLMENT', 40000) // INCREASED await this.humanDelay(10000, 15000) // INCREASED from 5-7s // Handle any popups after joining - with LONGER delays between log(false, 'CREATOR', 'Handling post-enrollment popups...', 'log', 'cyan') await this.humanDelay(5000, 8000) // INCREASED from 3-5s await this.handleRewardsWelcomeTour() await this.humanDelay(5000, 8000) // INCREASED from 3-5s await this.handleRewardsPopups() log(false, 'CREATOR', '✅ Rewards enrollment completed', 'log', 'green') } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `Warning: Could not complete enrollment: ${msg}`, 'warn', 'yellow') } } private async saveAccount(account: CreatedAccount): Promise { try { const accountsDir = path.join(process.cwd(), 'accounts-created') // Ensure directory exists if (!fs.existsSync(accountsDir)) { log(false, 'CREATOR', 'Creating accounts-created directory...', 'log', 'cyan') fs.mkdirSync(accountsDir, { recursive: true }) } // Create a unique filename for THIS account using timestamp and email const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\./g, '-') const emailPrefix = (account.email.split('@')[0] || 'account').substring(0, 20) // First 20 chars of email const filename = `account_${emailPrefix}_${timestamp}.jsonc` const filepath = path.join(accountsDir, filename) log(false, 'CREATOR', `Saving account to NEW file: ${filename}`, 'log', 'cyan') // Create account data with metadata const accountData = { ...account, savedAt: new Date().toISOString(), filename: filename } // Create output with comments const output = `// Microsoft Rewards - Account Created // Email: ${account.email} // Created: ${account.createdAt} // Saved: ${accountData.savedAt} ${JSON.stringify(accountData, null, 2)}` // Write to NEW file (never overwrites existing files) fs.writeFileSync(filepath, output, 'utf-8') // Verify the file was written correctly if (fs.existsSync(filepath)) { const verifySize = fs.statSync(filepath).size log(false, 'CREATOR', `✅ File written successfully (${verifySize} bytes)`, 'log', 'green') // Double-check we can read it back const verifyContent = fs.readFileSync(filepath, 'utf-8') const verifyJsonStartIndex = verifyContent.indexOf('{') const verifyJsonEndIndex = verifyContent.lastIndexOf('}') if (verifyJsonStartIndex !== -1 && verifyJsonEndIndex !== -1) { const verifyJsonContent = verifyContent.substring(verifyJsonStartIndex, verifyJsonEndIndex + 1) const verifyAccount = JSON.parse(verifyJsonContent) if (verifyAccount.email === account.email) { log(false, 'CREATOR', `✅ Verification passed: Account ${account.email} saved correctly`, 'log', 'green') } else { log(false, 'CREATOR', '⚠️ Verification warning: Email mismatch', 'warn', 'yellow') } } } else { log(false, 'CREATOR', '❌ File verification failed - file does not exist!', 'error') } log(false, 'CREATOR', `✅ Account saved successfully to: ${filepath}`, 'log', 'green') } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `❌ Error saving account: ${msg}`, 'error') // Try to save to a fallback file try { const fallbackPath = path.join(process.cwd(), `account-backup-${Date.now()}.jsonc`) fs.writeFileSync(fallbackPath, JSON.stringify(account, null, 2), 'utf-8') log(false, 'CREATOR', `⚠️ Account saved to fallback file: ${fallbackPath}`, 'warn', 'yellow') } catch (fallbackError) { const fallbackMsg = fallbackError instanceof Error ? fallbackError.message : String(fallbackError) log(false, 'CREATOR', `❌ Failed to save fallback: ${fallbackMsg}`, 'error') } } } async close(): Promise { if (!this.rlClosed) { this.rl.close() this.rlClosed = true } if (this.page && !this.page.isClosed()) { await this.page.close() } } }