From e27c70a021a809f78129ce1c423251489271c25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Braz=CC=87ewicz?= Date: Mon, 6 Jul 2026 14:56:02 +0200 Subject: [PATCH 1/2] fixed dogfooding google login --- dogfooding/lib/core/model/environment.dart | 3 + .../lib/core/repos/user_auth_repository.dart | 15 +- dogfooding/lib/di/injector.dart | 14 +- dogfooding/lib/screens/login_screen.dart | 163 +++++++++++++----- .../lib/widgets/environment_switcher.dart | 11 +- dogfooding/web/index.html | 20 --- 6 files changed, 146 insertions(+), 80 deletions(-) diff --git a/dogfooding/lib/core/model/environment.dart b/dogfooding/lib/core/model/environment.dart index 26e42e93d..28acf2570 100644 --- a/dogfooding/lib/core/model/environment.dart +++ b/dogfooding/lib/core/model/environment.dart @@ -70,6 +70,9 @@ enum Environment { final List aliases; final List baseUrls; + /// Whether this is a Pronto environment. + bool get isPronto => envName == 'pronto'; + String? getJoinUrl({required String callId, String? callType}) { switch (this) { case Environment.pronto: diff --git a/dogfooding/lib/core/repos/user_auth_repository.dart b/dogfooding/lib/core/repos/user_auth_repository.dart index 3bddab75e..dacfa09ce 100644 --- a/dogfooding/lib/core/repos/user_auth_repository.dart +++ b/dogfooding/lib/core/repos/user_auth_repository.dart @@ -17,13 +17,14 @@ class UserAuthRepository { Future login() async { final response = await videoClient.connect(); - return response.fold( - success: (success) { - return UserCredentials(token: success.data, userInfo: currentUser); - }, - failure: (failure) { - throw failure.error; - }, + if (response is Failure) { + await videoClient.disconnect(); + throw response.error; + } + + return UserCredentials( + token: (response as Success).data, + userInfo: currentUser, ); } diff --git a/dogfooding/lib/di/injector.dart b/dogfooding/lib/di/injector.dart index 5299b0c9d..2e35c9c77 100644 --- a/dogfooding/lib/di/injector.dart +++ b/dogfooding/lib/di/injector.dart @@ -26,11 +26,15 @@ class AppInjector { // Register dependencies static Future init({Environment? forceEnvironment}) async { - // Google sign in - locator.registerSingletonAsync(() async { - await GoogleSignIn.instance.initialize(hostedDomain: 'getstream.io'); - return GoogleSignIn.instance; - }); + // Google sign in. On web we use Firebase Auth's popup flow instead of the + // google_sign_in plugin (see LoginScreen), so we only initialize the + // plugin on native platforms. + if (!kIsWeb) { + locator.registerSingletonAsync(() async { + await GoogleSignIn.instance.initialize(hostedDomain: 'getstream.io'); + return GoogleSignIn.instance; + }); + } // App Preferences final prefs = await SharedPreferences.getInstance(); diff --git a/dogfooding/lib/screens/login_screen.dart b/dogfooding/lib/screens/login_screen.dart index 6ff35471e..65a4cfd42 100644 --- a/dogfooding/lib/screens/login_screen.dart +++ b/dogfooding/lib/screens/login_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math' as math; +import 'package:firebase_auth/firebase_auth.dart' as fb; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -28,48 +29,102 @@ class LoginScreen extends StatefulWidget { class _LoginScreenState extends State { final _appPreferences = locator(); - final _emailController = TextEditingController(); + final _usernameController = TextEditingController(); - Future _loginWithGoogle() async { - final googleService = locator(); + StreamSubscription? _googleAuthSubscription; + bool _isLoggingIn = false; + + @override + void initState() { + super.initState(); + unawaited(_initGoogleSignIn()); + } + + /// Subscribes to Google auth events so the result of the mobile/macOS + /// `authenticate()` call is handled. Web uses Firebase Auth's popup flow + /// instead (see [_loginWithGoogleWeb]), so there is no google_sign_in plugin + /// to subscribe to there. + Future _initGoogleSignIn() async { + if (kIsWeb) return; + + final googleService = await locator.getAsync(); + _googleAuthSubscription = googleService.authenticationEvents.listen( + _handleGoogleAuthEvent, + onError: (Object e) => debugPrint('Google auth event error: $e'), + ); + } + + void _handleGoogleAuthEvent(GoogleSignInAuthenticationEvent event) { + if (event is! GoogleSignInAuthenticationEventSignIn) return; + + final googleUser = event.user; + unawaited( + _completeGoogleLogin( + email: googleUser.email, + name: googleUser.displayName ?? '', + image: googleUser.photoUrl, + ), + ); + } - GoogleSignInAccount? googleUser; + /// Triggers an interactive Google sign-in from the button tap. + Future _loginWithGoogle() async { try { - googleUser = await googleService.attemptLightweightAuthentication(); - } catch (e) { - debugPrint('Google lightweight auth failed: $e'); - } + if (kIsWeb) { + await _loginWithGoogleWeb(); + return; + } - if (googleUser == null && googleService.supportsAuthenticate()) { - try { - googleUser = await googleService.authenticate(); - } catch (e) { - return debugPrint('Google sign-in failed: $e'); + final googleService = await locator.getAsync(); + if (googleService.supportsAuthenticate()) { + // Result delivered via authenticationEvents -> _handleGoogleAuthEvent. + await googleService.authenticate(); } + } catch (e) { + debugPrint('Google sign-in failed: $e'); } + } - if (googleUser == null) { - return debugPrint('Google login cancelled or unavailable'); + /// Web Google sign-in via Firebase Auth's popup flow. + Future _loginWithGoogleWeb() async { + final provider = fb.GoogleAuthProvider() + // Hints Google to restrict the account chooser to the Stream domain. + ..setCustomParameters(const {'hd': 'getstream.io'}); + + final credential = await fb.FirebaseAuth.instance.signInWithPopup(provider); + final user = credential.user; + if (user == null) { + return debugPrint('Google sign-in returned no user'); } + await _completeGoogleLogin( + email: user.email, + name: user.displayName ?? '', + image: user.photoURL, + ); + } + + Future _completeGoogleLogin({ + required String? email, + required String name, + String? image, + }) async { final userInfo = UserInfo( - role: 'admin', - id: createValidId(googleUser.email), - name: googleUser.displayName ?? '', - image: googleUser.photoUrl, + id: createValidId(email!), + name: name, + image: image, ); - return _login(User(info: userInfo), _appPreferences.environment); + await _login(User(info: userInfo), _appPreferences.environment); } - Future _loginWithEmail() async { - final email = _emailController.text; - if (email.isEmpty) return debugPrint('Email is empty'); + Future _loginWithUsername() async { + final username = _usernameController.text; + if (username.isEmpty) return debugPrint('Username is empty'); final userInfo = UserInfo( - role: 'admin', - id: createValidId(email), - name: email, + id: createValidId(username), + name: username, ); return _login(User(info: userInfo), _appPreferences.environment); @@ -78,7 +133,6 @@ class _LoginScreenState extends State { Future _loginAsGuest() async { final userId = randomId(size: 6); final userInfo = UserInfo( - role: 'admin', id: userId, name: userId, image: @@ -92,6 +146,9 @@ class _LoginScreenState extends State { } Future _login(User user, Environment environment) async { + if (_isLoggingIn) return; + _isLoggingIn = true; + if (mounted) unawaited(showLoadingIndicator(context)); // Register StreamVideo client with the user. @@ -100,21 +157,27 @@ class _LoginScreenState extends State { try { await authController.login(user, environment); } catch (e, _) { - if (mounted) { - hideLoadingIndicator(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(seconds: 20), - content: Text('Error: $e'), - ), - ); - } + if (mounted) hideLoadingIndicator(context); + _showSnackBar('Error: $e'); + } finally { + _isLoggingIn = false; } } + void _showSnackBar(String message) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 20), + content: Text(message), + ), + ); + } + @override void dispose() { - _emailController.dispose(); + unawaited(_googleAuthSubscription?.cancel()); + _usernameController.dispose(); super.dispose(); } @@ -131,6 +194,8 @@ class _LoginScreenState extends State { if (!kIsProd) EnvironmentSwitcher( currentEnvironment: _appPreferences.environment, + // Rebuild so the Google button visibility tracks the environment. + onEnvironmentChanged: (_) => setState(() {}), ), ], ), @@ -162,12 +227,12 @@ class _LoginScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: TextField( - controller: _emailController, + controller: _usernameController, style: theme.textTheme.bodyMedium?.apply( color: Colors.white, ), decoration: const InputDecoration( - labelText: 'Enter Email', + labelText: 'Enter Username', isDense: true, border: OutlineInputBorder(), ), @@ -177,12 +242,12 @@ class _LoginScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamButton.active( - label: 'Sign up with email', + label: 'Sign up with username', icon: const Icon( - Icons.email_outlined, + Icons.person_outline, color: Colors.white, ), - onPressed: _loginWithEmail, + onPressed: _loginWithUsername, ), ), const SizedBox(height: 16), @@ -203,11 +268,15 @@ class _LoginScreenState extends State { ], ), ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: GoogleLoginButton(onPressed: _loginWithGoogle), - ), + // Google (Stream employee) sign-in is only available on + // Pronto environments. + if (_appPreferences.environment.isPronto) ...[ + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: GoogleLoginButton(onPressed: _loginWithGoogle), + ), + ], const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), diff --git a/dogfooding/lib/widgets/environment_switcher.dart b/dogfooding/lib/widgets/environment_switcher.dart index 7299aacbf..070b66543 100644 --- a/dogfooding/lib/widgets/environment_switcher.dart +++ b/dogfooding/lib/widgets/environment_switcher.dart @@ -7,10 +7,17 @@ import '../di/injector.dart'; import '../theme/app_palette.dart'; class EnvironmentSwitcher extends StatefulWidget { - const EnvironmentSwitcher({super.key, required this.currentEnvironment}); + const EnvironmentSwitcher({ + super.key, + required this.currentEnvironment, + this.onEnvironmentChanged, + }); final Environment currentEnvironment; + /// Called after the user selects a different environment. + final ValueChanged? onEnvironmentChanged; + @override State createState() => _EnvironmentSwitcherState(); } @@ -84,6 +91,8 @@ class _EnvironmentSwitcherState extends State { setState(() { selectedEnvironment = env; }); + + widget.onEnvironmentChanged?.call(env); }, child: Container( width: 100, diff --git a/dogfooding/web/index.html b/dogfooding/web/index.html index cb0f84ae7..d66caa627 100644 --- a/dogfooding/web/index.html +++ b/dogfooding/web/index.html @@ -27,11 +27,6 @@ - - - - @@ -62,21 +57,6 @@ }); - - - - From 4f9176c8571cae679da2ec59a363b220d004b385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Braz=CC=87ewicz?= Date: Mon, 6 Jul 2026 15:56:06 +0200 Subject: [PATCH 2/2] handle login errors --- dogfooding/lib/screens/login_screen.dart | 50 ++++++++++++++++-------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/dogfooding/lib/screens/login_screen.dart b/dogfooding/lib/screens/login_screen.dart index 65a4cfd42..c4ca1caea 100644 --- a/dogfooding/lib/screens/login_screen.dart +++ b/dogfooding/lib/screens/login_screen.dart @@ -50,7 +50,10 @@ class _LoginScreenState extends State { final googleService = await locator.getAsync(); _googleAuthSubscription = googleService.authenticationEvents.listen( _handleGoogleAuthEvent, - onError: (Object e) => debugPrint('Google auth event error: $e'), + onError: (Object e) { + debugPrint('Google auth event error: $e'); + _showSnackBar('Google sign-in failed: $e'); + }, ); } @@ -82,26 +85,35 @@ class _LoginScreenState extends State { } } catch (e) { debugPrint('Google sign-in failed: $e'); + _showSnackBar('Google sign-in failed: $e'); } } /// Web Google sign-in via Firebase Auth's popup flow. Future _loginWithGoogleWeb() async { - final provider = fb.GoogleAuthProvider() - // Hints Google to restrict the account chooser to the Stream domain. - ..setCustomParameters(const {'hd': 'getstream.io'}); - - final credential = await fb.FirebaseAuth.instance.signInWithPopup(provider); - final user = credential.user; - if (user == null) { - return debugPrint('Google sign-in returned no user'); - } + try { + final provider = fb.GoogleAuthProvider() + // Hints Google to restrict the account chooser to the Stream domain. + ..setCustomParameters(const {'hd': 'getstream.io'}); + + final credential = + await fb.FirebaseAuth.instance.signInWithPopup(provider); + final user = credential.user; + if (user == null) { + debugPrint('Google sign-in returned no user'); + _showSnackBar('Google sign-in failed: no user returned.'); + return; + } - await _completeGoogleLogin( - email: user.email, - name: user.displayName ?? '', - image: user.photoURL, - ); + await _completeGoogleLogin( + email: user.email, + name: user.displayName ?? '', + image: user.photoURL, + ); + } catch (e) { + debugPrint('Google sign-in failed: $e'); + _showSnackBar('Google sign-in failed: $e'); + } } Future _completeGoogleLogin({ @@ -109,8 +121,14 @@ class _LoginScreenState extends State { required String name, String? image, }) async { + if (email == null || email.isEmpty) { + debugPrint('Google sign-in returned no email'); + _showSnackBar('Google sign-in failed: no email address returned.'); + return; + } + final userInfo = UserInfo( - id: createValidId(email!), + id: createValidId(email), name: name, image: image, );