From c9a11e9c829d239d4316dee64c3cfc81b276af56 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Mon, 16 Feb 2026 15:23:41 +0100
Subject: [PATCH] fix: add retry mechanism (5x/5s) to auth pre-join
verification and Join button click, verify no name input on auth page
Co-authored-by: Cursor
---
src/bot/joinProcedure.ts | 107 +++++++++++++++++++++++++++------------
1 file changed, 74 insertions(+), 33 deletions(-)
diff --git a/src/bot/joinProcedure.ts b/src/bot/joinProcedure.ts
index f10cf24..28f29ec 100644
--- a/src/bot/joinProcedure.ts
+++ b/src/bot/joinProcedure.ts
@@ -129,10 +129,11 @@ export class JoinProcedure {
this._logger.info(`Starting lobby join flow... (authenticated: ${this._isAuthenticated})`);
if (this._isAuthenticated) {
- // Authenticated join: name comes from Microsoft account, no name input needed
- // Wait for the pre-join page to load (look for Join now button)
- this._logger.info('Authenticated join - skipping name input, waiting for Join button...');
- await this._page.waitForTimeout(3000);
+ // Authenticated join: wait for the authenticated pre-join page.
+ // Proof that we're on the RIGHT page: no name input field exists
+ // (the anonymous page has input[placeholder="Type your name"]).
+ // Retry up to 5 times, every 5 seconds.
+ await this._waitForAuthenticatedPreJoinPage();
} else {
// Anonymous join: enter bot name in the name input field
await this._enterBotName();
@@ -142,6 +143,41 @@ export class JoinProcedure {
await this._clickJoinNow();
}
+ /**
+ * Wait for the authenticated pre-join page to be ready.
+ *
+ * Verification: The authenticated page does NOT have a name input field.
+ * If a name input (placeholder="Type your name") exists, we're still on the
+ * anonymous page and need to wait for the redirect to complete.
+ *
+ * Retries 5 times, every 5 seconds.
+ */
+ private async _waitForAuthenticatedPreJoinPage(): Promise {
+ const maxRetries = 5;
+ const retryIntervalMs = 5000;
+
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
+ const url = this._page.url();
+ const hasNameInput = await this._page.$('input[placeholder="Type your name"]');
+ const hasJoinButton = await this._page.$('#prejoin-join-button, button[data-tid="prejoin-join-button"], button:has-text("Join now")');
+
+ if (hasJoinButton && !hasNameInput) {
+ this._logger.info(`Authenticated pre-join page confirmed (attempt ${attempt}/${maxRetries}): Join button present, no name input. URL: ${url.substring(0, 100)}`);
+ return;
+ }
+
+ const nameStatus = hasNameInput ? 'name input FOUND (wrong page)' : 'no name input';
+ const joinStatus = hasJoinButton ? 'Join button found' : 'no Join button';
+ this._logger.info(`Waiting for authenticated pre-join page (attempt ${attempt}/${maxRetries}): ${nameStatus}, ${joinStatus}. URL: ${url.substring(0, 100)}`);
+
+ if (attempt < maxRetries) {
+ await this._page.waitForTimeout(retryIntervalMs);
+ }
+ }
+
+ this._logger.warn('Could not confirm authenticated pre-join page after all retries. Proceeding anyway...');
+ }
+
/**
* Enter the bot name in the name input field.
* Primary selector: input[placeholder="Type your name"] (confirmed by Recall.ai).
@@ -200,56 +236,61 @@ export class JoinProcedure {
private async _clickJoinNow(): Promise {
this._logger.info('Clicking Join now...');
- // First, dismiss any "no audio/video" modal that may be blocking
- await this._dismissNoAudioVideoModal();
+ const maxRetries = 5;
+ const retryIntervalMs = 5000;
- // Teams v2 uses stable IDs for the join button. Wait with multiple selectors.
- // The button may take time to render in the Teams v2 SPA after auth redirect.
const joinSelectors = [
'#prejoin-join-button', // Teams v2 stable ID
'button[data-tid="prejoin-join-button"]', // Teams v2 data-tid
'button:has-text("Join now")', // Text-based (light-meetings)
'button:has-text("Join meeting")', // Alternative text
];
-
const combinedSelector = joinSelectors.join(', ');
- try {
- await this._page.waitForSelector(combinedSelector, { timeout: 20000, state: 'visible' });
- const button = await this._page.$(combinedSelector);
- if (button) {
- await button.click();
- this._logger.info('Clicked "Join now" button');
- await this._page.waitForTimeout(2000);
- await this._dismissNoAudioVideoModal();
- return;
- }
- } catch {
- this._logger.info('Join button not found with combined selectors, trying text fallbacks...');
- }
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
+ // Dismiss any "no audio/video" modal that may be blocking
+ await this._dismissNoAudioVideoModal();
- // Last resort fallback: any button with "Join" text
- const fallbackSelectors = [
- 'button:has-text("Join")',
- '[data-tid="joinButton"]',
- ];
-
- for (const selector of fallbackSelectors) {
try {
- const button = await this._page.$(selector);
+ await this._page.waitForSelector(combinedSelector, { timeout: 5000, state: 'visible' });
+ const button = await this._page.$(combinedSelector);
if (button) {
await button.click();
- this._logger.info(`Clicked join button (fallback: ${selector})`);
+ this._logger.info(`Clicked "Join now" button (attempt ${attempt}/${maxRetries})`);
await this._page.waitForTimeout(2000);
await this._dismissNoAudioVideoModal();
return;
}
} catch {
- // Continue
+ // Button not found this attempt
+ }
+
+ // Fallback: any button with "Join" text
+ const fallbackSelectors = ['button:has-text("Join")', '[data-tid="joinButton"]'];
+ for (const selector of fallbackSelectors) {
+ try {
+ const button = await this._page.$(selector);
+ if (button && await button.isVisible()) {
+ await button.click();
+ this._logger.info(`Clicked join button fallback: ${selector} (attempt ${attempt}/${maxRetries})`);
+ await this._page.waitForTimeout(2000);
+ await this._dismissNoAudioVideoModal();
+ return;
+ }
+ } catch {
+ // Continue
+ }
+ }
+
+ const url = this._page.url();
+ this._logger.info(`Join button not found (attempt ${attempt}/${maxRetries}). URL: ${url.substring(0, 100)}`);
+
+ if (attempt < maxRetries) {
+ await this._page.waitForTimeout(retryIntervalMs);
}
}
- // Diagnostic info for debugging
+ // All retries exhausted — throw with diagnostic info
const currentUrl = this._page.url();
const title = await this._page.title();
const bodyText = await this._page.evaluate(() =>