diff --git a/backend/api_core.py b/backend/api_core.py index f515d27..be3f116 100644 --- a/backend/api_core.py +++ b/backend/api_core.py @@ -8108,6 +8108,7 @@ def _handle_routes(db): ep = db.execute("SELECT * FROM employer_profiles WHERE user_id = ?", [user_id]).fetchone() user_data['worker_profile'] = row_to_dict(wp) user_data['employer_profile'] = row_to_dict(ep) + user_data['is_new_user'] = is_new_google_user return json_response(user_data, 200 if existing else 201) diff --git a/backend/test_deep_audit_regressions.py b/backend/test_deep_audit_regressions.py index 9af1148..aa05f2c 100644 --- a/backend/test_deep_audit_regressions.py +++ b/backend/test_deep_audit_regressions.py @@ -71,6 +71,35 @@ def _request_api(self, method, path, payload=None, token=""): self.module.handle_request() return parse_cgi_output(out.getvalue()) + def test_google_auth_reports_whether_the_account_was_created(self): + os.environ["GOOGLE_CLIENT_ID"] = "google-client-under-test" + verified_identity = { + "aud": "google-client-under-test", + "iss": "https://accounts.google.com", + "email": "new-google-user@example.com", + "email_verified": "true", + "name": "New Google User", + "sub": "google-sub-under-test", + } + response = mock.MagicMock() + response.__enter__.return_value.read.return_value = json.dumps(verified_identity).encode("utf-8") + + with ( + mock.patch.object(self.module.urllib.request, "urlopen", return_value=response), + mock.patch.object(self.module, "send_welcome_email"), + ): + created_status, created = self._request_api( + "POST", "/auth/google", {"credential": "new-google-credential"} + ) + existing_status, existing = self._request_api( + "POST", "/auth/google", {"credential": "returning-google-credential"} + ) + + self.assertEqual(created_status, 201, created) + self.assertIs(created["is_new_user"], True) + self.assertEqual(existing_status, 200, existing) + self.assertIs(existing["is_new_user"], False) + def test_service_mutations_enforce_checkout_compatible_delivery_days(self): token = "tok-service-delivery" db = self.module.get_db() diff --git a/frontend/index.html b/frontend/index.html index 9f3ddfb..822b01a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -362,10 +362,16 @@

Common agent-delegated work

function handleGoogleCredential(response) { api('/auth/google', { method: 'POST', body: { credential: response.credential } }) .then(data => { - const { token, ...user } = data; + const { token, is_new_user: isNewUser, ...user } = data; saveSession(token, user); - toast('Welcome to GoHireHumans!', 'success'); - trackRecommendedEvent('login', { method: 'google' }); + if (isNewUser) { + trackEvent('signup_completed', { source: 'google_auth', method: 'google' }); + trackRecommendedEvent('sign_up', { method: 'google', source: 'google_auth' }); + toast('Account created! Welcome to GoHireHumans.', 'success'); + } else { + trackRecommendedEvent('login', { method: 'google' }); + toast('Welcome back!', 'success'); + } navigate('#/'); }) .catch(err => toast(err.message || 'Google sign-in failed', 'error')); diff --git a/frontend/tests/browser-regression.spec.js b/frontend/tests/browser-regression.spec.js index 0ca898c..03c2ca7 100644 --- a/frontend/tests/browser-regression.spec.js +++ b/frontend/tests/browser-regression.spec.js @@ -239,6 +239,50 @@ test.describe('GoHireHumans public/browser regression suite', () => { expect(call[2]).not.toHaveProperty('campaign'); }); + test('Google auth distinguishes completed signup from returning login', async ({ page }) => { + await setupDeterministicLocalPage(page); + await page.goto('/', { waitUntil: 'domcontentloaded' }); + + const eventsFor = async isNewUser => page.evaluate(async newAccount => { + const events = []; + window.gtag = (...args) => events.push(args); + window.api = async path => { + if (path !== '/auth/google') throw new Error(`Unexpected API path: ${path}`); + return { + id: newAccount ? 701 : 702, + email: newAccount ? 'new-google@example.test' : 'returning-google@example.test', + name: newAccount ? 'New Google User' : 'Returning Google User', + token: 'google-auth-test-token', + is_new_user: newAccount, + }; + }; + window.saveSession = () => {}; + window.toast = () => {}; + window.navigate = () => {}; + handleGoogleCredential({ credential: 'google-credential-under-test' }); + for (let attempt = 0; attempt < 20 && events.length === 0; attempt += 1) { + await new Promise(resolve => setTimeout(resolve, 10)); + } + return events + .filter(args => args[0] === 'event') + .map(args => ({ name: args[1], params: args[2] })); + }, isNewUser); + + const newAccountEvents = await eventsFor(true); + expect(newAccountEvents.map(event => event.name)).toEqual(['signup_completed', 'sign_up']); + expect(newAccountEvents.find(event => event.name === 'signup_completed')?.params).toMatchObject({ + ui_source: 'google_auth', + method: 'google', + }); + expect(newAccountEvents.map(event => event.name)).not.toContain('login'); + + const returningEvents = await eventsFor(false); + expect(returningEvents.map(event => event.name)).toEqual(['login']); + expect(returningEvents[0]?.params).toMatchObject({ method: 'google' }); + expect(returningEvents.map(event => event.name)).not.toContain('sign_up'); + expect(returningEvents.map(event => event.name)).not.toContain('signup_completed'); + }); + test('application submission failure stays visible and retryable', async ({ page }) => { await setupDeterministicLocalPage(page); await page.goto('/', { waitUntil: 'domcontentloaded' });