From ba0bb237d81afc0ca5753072b925e3551813260f Mon Sep 17 00:00:00 2001 From: Seenu K Date: Wed, 12 Aug 2026 13:22:20 +0530 Subject: [PATCH] Let a game name its own host on the web sign-in screen Web sign-in runs through Firebase's popup, which loads `https:///__/auth/handler`, so Google's account chooser reads "Sign in to continue to my-project.firebaseapp.com". `EngineConfig.authDomain` replaces that host. Optional, and unset is the right answer for almost every game: sign-in works on the Firebase project's own domain, and Android never shows the string at all, because it signs in through the native Google flow rather than the popup. Applied to the generated `FirebaseOptions` at startup rather than written into `firebase_options.dart`, which FlutterFire regenerates and would drop the edit on the next `configure_firebase`. Validated as a bare host like `appHost`: a malformed value would otherwise reach Firebase as the origin it builds the popup URL from, and surface as a sign-in that never returns. It is not `appHost`. The value must be a domain Firebase Hosting answers for, a sibling of the game's own Worker host rather than the host itself. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 5 ++++ lib/app_runner.dart | 11 +++++++- lib/core/config/app_config.dart | 34 +++++++++++++++++++++++++ test/core/config/app_config_test.dart | 36 +++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c0fe81..53c0b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ Pre-1.0, breaking changes land in a **MINOR** bump: `^0.1.0` resolves to [Versions and compatibility](https://eigeninteractive.com/docs/reference/compatibility) for how this package, the engine and the generated `eigen_api` client pair up. +## [Unreleased] +### Added +- EngineConfig.authDomain overrides Firebase Auth's domain, so web sign-in can name your own host instead of the project's firebaseapp.com. Optional and cosmetic: unset, which is the scaffolded value, keeps the project default, and Android never shows it. It is not APP\_HOST; the value must be a Firebase Hosting domain. Applied to the generated FirebaseOptions at startup, so configure\_firebase can keep regenerating firebase\_options.dart. + ## [0.4.0] - 2026-08-11 ### Added - GameContentContext.transition gives a game the step into the current frame (from, to), or null when there is nothing to animate: a cold load, a rejoin, or the opening frame. Animate only when it is non-null, which replaces tracking the last rendered version in widget state. Replay supplies it too, so one animation path serves live play and replay. @@ -162,6 +166,7 @@ server-side concern now live in the engine. - `google_fonts`, which fetched Inter at runtime, replaced by the bundled package font above. +[Unreleased]: https://github.com/eigeninteractive/eigen-flutter/compare/v0.4.0...HEAD [0.4.0]: https://github.com/eigeninteractive/eigen-flutter/compare/v0.3.7...v0.4.0 [0.3.7]: https://github.com/eigeninteractive/eigen-flutter/compare/v0.3.6...v0.3.7 [0.3.6]: https://github.com/eigeninteractive/eigen-flutter/compare/v0.3.5...v0.3.6 diff --git a/lib/app_runner.dart b/lib/app_runner.dart index 84fa036..3bf3719 100644 --- a/lib/app_runner.dart +++ b/lib/app_runner.dart @@ -41,7 +41,16 @@ Future runEngineApp({ configureUrlStrategy(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - await Firebase.initializeApp(options: firebaseOptions); + // `firebase_options.dart` is generated by FlutterFire and regenerated by + // `configure_firebase`, so a hand-edited `authDomain` would not survive. The + // override is applied here instead, from ordinary deployment configuration. + // Unset is the normal case and leaves the generated options untouched. + final authDomain = config.engine.authDomain; + await Firebase.initializeApp( + options: authDomain == null + ? firebaseOptions + : firebaseOptions.copyWith(authDomain: authDomain), + ); // Crash reporting and analytics are disabled in non-release builds so dev // events never reach production dashboards. Crashlytics has no web // implementation; browser errors remain visible in the console and hosting diff --git a/lib/core/config/app_config.dart b/lib/core/config/app_config.dart index 5a9b6ac..15071de 100644 --- a/lib/core/config/app_config.dart +++ b/lib/core/config/app_config.dart @@ -41,6 +41,7 @@ class EngineConfig { required this.googleWebClientId, required this.firebaseVapidKey, this.appHost, + this.authDomain, }); /// Origin of the EigenInteractive server, with no trailing slash and no @@ -72,6 +73,28 @@ class EngineConfig { /// rather than being intercepted. final String? appHost; + /// Firebase Auth's own domain, overriding the project default; null keeps it. + /// + /// Purely cosmetic, and only in a browser. Web sign-in runs through Firebase's + /// popup, which loads `https:///__/auth/handler`, and Google's + /// account chooser names that host: by default *"Sign in to continue to + /// my-project.firebaseapp.com"*. Setting this replaces the string a player + /// reads there. Android signs in through the native Google flow and never + /// shows it, so an Android-only game has no reason to set this. + /// + /// **Not [appHost].** This host serves Firebase's auth handler and must be a + /// **Firebase Hosting** domain, whereas [appHost] is the game's own Worker. + /// One cannot be the other; they are sibling names on the same domain, e.g. + /// `auth.mygame.com` beside `mygame.com`. Setting this to a host Firebase + /// Hosting does not answer for breaks web sign-in outright, which is why it + /// stays unset until the Hosting domain exists. + /// + /// The name is only half the branding. The logo and product name on that + /// screen come from the OAuth consent screen in Google Cloud, which is a + /// separate change and needs no code. See + /// . + final String? authDomain; + /// Validates the deployment values before any engine service starts. /// /// [isWeb] controls the platform-specific Web Push requirement. Invalid @@ -108,6 +131,17 @@ class EngineConfig { errors.add('APP_HOST must be a hostname without a scheme, port, or path'); } + // Same shape as APP_HOST, and worth failing over rather than passing + // through: a malformed value reaches Firebase as the origin it builds the + // popup URL from, and the failure surfaces as a sign-in that never returns + // rather than as a configuration error. + final auth = authDomain; + if (auth != null && !_isValidHost(auth)) { + errors.add( + 'AUTH_DOMAIN must be a hostname without a scheme, port, or path', + ); + } + if (errors.isEmpty) return; throw StateError( 'Invalid EigenInteractive app configuration:\n' diff --git a/test/core/config/app_config_test.dart b/test/core/config/app_config_test.dart index 4428fc3..5cbd25e 100644 --- a/test/core/config/app_config_test.dart +++ b/test/core/config/app_config_test.dart @@ -84,5 +84,41 @@ void main() { ), ); }); + + test('treats a custom auth domain as optional', () { + // Every game ships without one: sign-in works on the Firebase project's + // own domain, and this only changes the hostname a web player reads on + // Google's account chooser. + const config = EngineConfig( + apiBaseUrl: 'https://game.example.com', + googleWebClientId: 'client.apps.googleusercontent.com', + firebaseVapidKey: 'public-vapid-key', + ); + + expect(() => config.validate(isWeb: true), returnsNormally); + }); + + test('rejects an auth domain given as a URL rather than a host', () { + // Firebase builds `https:///__/auth/handler` from this, so a + // value carrying its own scheme yields a popup URL that goes nowhere. + // Failing at startup beats a sign-in that silently never returns. + const config = EngineConfig( + apiBaseUrl: 'https://game.example.com', + googleWebClientId: 'client.apps.googleusercontent.com', + firebaseVapidKey: 'public-vapid-key', + authDomain: 'https://auth.game.example.com/', + ); + + expect( + () => config.validate(isWeb: true), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('AUTH_DOMAIN must be a hostname'), + ), + ), + ); + }); }); }