Skip to content
This repository was archived by the owner on Sep 10, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion lib/app_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,16 @@ Future<void> 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
Expand Down
34 changes: 34 additions & 0 deletions lib/core/config/app_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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://<authDomain>/__/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
/// <https://eigeninteractive.com/docs/ship-it/deploy-the-web-app>.
final String? authDomain;

/// Validates the deployment values before any engine service starts.
///
/// [isWeb] controls the platform-specific Web Push requirement. Invalid
Expand Down Expand Up @@ -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'
Expand Down
36 changes: 36 additions & 0 deletions test/core/config/app_config_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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://<authDomain>/__/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<StateError>().having(
(error) => error.message,
'message',
contains('AUTH_DOMAIN must be a hostname'),
),
),
);
});
});
}