44
55import type { Response , BrowserContext , Page } from 'playwright' ;
66import { ApiCredentialStatus , ApiCredentials , OAuthCredentials } from '../apiCredentials.js' ;
7+ import { generateLatchkeyAppName } from '../playwrightUtils.js' ;
78import { runCaptured } from '../curl.js' ;
89import { Service , BrowserFollowupServiceSession , LoginFailedError } from './base.js' ;
910import * as http from 'node:http' ;
1011import * as url from 'node:url' ;
1112
12- const DEFAULT_TIMEOUT_MS = 30000 ;
13+ const DEFAULT_TIMEOUT_MS = 8000 ;
1314const OAUTH_CALLBACK_PORT = 8080 ;
1415const OAUTH_SCOPES = [
1516 // User info (for credential validation)
@@ -259,10 +260,10 @@ class GoogleServiceSession extends BrowserFollowupServiceSession {
259260 clientSecret = oldCredentials . clientSecret ;
260261 } else {
261262 // Step 1: Navigate to Google Cloud Console and create project
262- await this . createProject ( page ) ;
263+ const projectSlug = await this . createProject ( page ) ;
263264
264265 // Step 2: Enable required APIs
265- await this . enableGoogleApis ( page ) ;
266+ await this . enableGoogleApis ( page , projectSlug ) ;
266267
267268 // Step 3: Create OAuth client ID
268269 const credentials = await this . createOAuthClient ( page ) ;
@@ -286,34 +287,38 @@ class GoogleServiceSession extends BrowserFollowupServiceSession {
286287 ) ;
287288 }
288289
289- private async createProject ( page : Page ) : Promise < void > {
290+ private async createProject ( page : Page ) : Promise < string > {
290291 // Navigate to the projects page
291292 await page . goto ( 'https://console.cloud.google.com/projectselector2/home/dashboard' , {
292293 timeout : DEFAULT_TIMEOUT_MS ,
293294 } ) ;
294295
295- // Wait a bit for the page to load
296- await page . waitForTimeout ( 2000 ) ;
297-
298296 // Always create a new project
299- const createProjectButton = page . locator ( 'text="Create Project"' ) . first ( ) ;
300-
301- if ( await createProjectButton . isVisible ( { timeout : 5000 } ) ) {
302- await createProjectButton . click ( ) ;
303- await page . waitForTimeout ( 1000 ) ;
297+ const createProjectButton = page . locator ( '.projectselector-project-create' ) ;
298+ await createProjectButton . waitFor ( { timeout : DEFAULT_TIMEOUT_MS } ) ;
299+ await createProjectButton . click ( ) ;
304300
305- const projectNameInput = page . locator ( 'input[name="projectName"]' ) ;
306- await projectNameInput . fill ( 'Latchkey Project' ) ;
301+ const projectNameInput = page . locator ( 'input[name="projectName"]' ) ;
302+ await projectNameInput . waitFor ( { timeout : DEFAULT_TIMEOUT_MS } ) ;
303+ await projectNameInput . fill ( generateLatchkeyAppName ( ) ) ;
307304
308- const createButton = page . locator ( 'button:has-text("Create") ' ) ;
309- await createButton . click ( ) ;
305+ const createButton = page . locator ( 'button[type="submit"] ' ) ;
306+ await createButton . click ( ) ;
310307
311- // Wait for project creation
312- await page . waitForTimeout ( 5000 ) ;
308+ // Wait until the URL is https:
309+ // https://console.cloud.google.com/home/dashboard?project=...
310+ await page . waitForURL ( 'https://console.cloud.google.com/home/dashboard?project=**' , {
311+ timeout : 16000 ,
312+ } ) ;
313+ const urlObj = new URL ( page . url ( ) ) ;
314+ const projectId = urlObj . searchParams . get ( 'project' ) ;
315+ if ( ! projectId ) {
316+ throw new LoginFailedError ( 'Failed to create or retrieve Google Cloud project ID.' ) ;
313317 }
318+ return projectId ;
314319 }
315320
316- private async enableGoogleApis ( page : Page ) : Promise < void > {
321+ private async enableGoogleApis ( page : Page , projectSlug : string ) : Promise < void > {
317322 const apis = [
318323 'gmail.googleapis.com' ,
319324 'calendar-json.googleapis.com' ,
@@ -324,31 +329,32 @@ class GoogleServiceSession extends BrowserFollowupServiceSession {
324329 ] ;
325330
326331 for ( const api of apis ) {
327- await this . enableApi ( page , api ) ;
332+ await this . enableApi ( page , projectSlug , api ) ;
328333 }
329334 }
330335
331- private async enableApi ( page : Page , apiName : string ) : Promise < void > {
332- // Navigate to API library
333- await page . goto ( `https://console.cloud.google.com/apis/library/${ apiName } ` , {
336+ private async enableApi ( page : Page , projectSlug : string , apiName : string ) : Promise < void > {
337+ await page . goto ( `https://console.cloud.google.com/apis/library/${ apiName } ?project=${ projectSlug } ` , {
334338 timeout : DEFAULT_TIMEOUT_MS ,
335339 } ) ;
336340
337- await page . waitForTimeout ( 2000 ) ;
341+ const manageButton = page . locator ( 'text="Manage"' ) ;
342+ const enableButton = page . locator ( 'button:has-text("Enable")' ) ;
343+ const manageOrEnableButton = manageButton . or ( enableButton ) ;
344+
345+ await manageOrEnableButton . isVisible ( { timeout : DEFAULT_TIMEOUT_MS } ) ;
338346
339347 // Check if API is already enabled
340- const manageButton = page . locator ( 'text="Manage"' ) ;
341- if ( await manageButton . isVisible ( { timeout : 5000 } ) ) {
342- // API already enabled
348+ if ( await manageButton . isVisible ( ) ) {
343349 return ;
344350 }
345351
346- // Enable the API
347- const enableButton = page . locator ( 'button:has-text("Enable")' ) ;
348- if ( await enableButton . isVisible ( { timeout : 5000 } ) ) {
352+ if ( await enableButton . isVisible ( ) ) {
349353 await enableButton . click ( ) ;
350- await page . waitForTimeout ( 5000 ) ;
351354 }
355+ const disableButton = page . locator ( 'text="Disable API"' ) ;
356+ await disableButton . waitFor ( { timeout : 16000 } ) ;
357+ throw new LoginFailedError ( `Failed to enable API: ${ apiName } ` ) ;
352358 }
353359
354360 private async createOAuthClient ( page : Page ) : Promise < { clientId : string ; clientSecret : string } > {
0 commit comments