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'), + ), + ), + ); + }); }); }