diff --git a/apps/app_seeds/lib/di/injector.dart b/apps/app_seeds/lib/di/injector.dart index f96d188..79537e2 100644 --- a/apps/app_seeds/lib/di/injector.dart +++ b/apps/app_seeds/lib/di/injector.dart @@ -40,15 +40,33 @@ import '../services/social_settings.dart'; /// in the tree. final GetIt getIt = GetIt.instance; +/// End-of-init sentinel: registered last, so `isRegistered<_DepsReady>()` means +/// the container is fully wired (see [configureDependencies]). Distinct from +/// [SocialService], which is optional and absent in inventory-only mode. +class _DepsReady { + const _DepsReady(); +} + /// Wires the encrypted DB, keystore and repositories. Call once from `main` /// before `runApp`; the DB key must exist before the DB opens. /// -/// Idempotent: a hot restart (or an Android Activity restart that keeps the -/// isolate) re-runs `main` while GetIt still holds the singletons. Without this -/// guard the re-registration throws, `main` aborts before rebuilding `TaneApp` -/// with the social layer, and the app falls back to the "inventory only" look. +/// Idempotent AND all-or-nothing: a hot restart (or an Android Activity restart +/// that keeps the isolate) re-runs `main` while GetIt still holds the singletons. +/// +/// A dedicated [_DepsReady] marker is registered LAST and used as the "fully +/// wired" sentinel, so `isRegistered<_DepsReady>()` means the WHOLE container is +/// ready — never a half-built one. If a prior run crashed part-way through init +/// (leaving some singletons registered but not the marker), we reset the +/// container and rebuild from scratch rather than early-returning on a partial +/// container or throwing "already registered" mid-cascade. That partial state is +/// safe to reset because `main` aborts before `runApp` on a partial run, so no +/// live UI holds these singletons. Without this, the app intermittently fell +/// back to the "inventory only" look (market/chat/profile greyed out). Future configureDependencies() async { - if (getIt.isRegistered()) return; + if (getIt.isRegistered<_DepsReady>()) return; // fully wired already + // A prior run half-wired the container (crashed after some registrations but + // before the sentinel). Clear it so we rebuild cleanly. + if (getIt.isRegistered()) await getIt.reset(); final secretStore = FlutterSecretStore(); final keyStore = SecureKeyStore(store: secretStore); @@ -66,8 +84,15 @@ Future configureDependencies() async { // Block 2 social identity: a secp256k1 Nostr key derived (one-way) from the // SAME root seed, so it needs no extra backup. Cheap + offline; no relay is - // contacted here (local-first — the social layer only enriches). - final socialService = await SocialService.fromRootSeedHex(rootSeedHex); + // contacted here (local-first — the social layer only enriches). Non-fatal: if + // the derivation ever fails, the app still opens on inventory (market/chat/ + // profile stay hidden) rather than blanking the whole app. + SocialService? socialService; + try { + socialService = await SocialService.fromRootSeedHex(rootSeedHex); + } catch (e, s) { + debugPrint('Social identity derivation failed; inventory-only mode: $e\n$s'); + } // Seed the bundled species catalog before the UI opens — but only once per // catalog version. Parsing the ~1.5 MB catalog and scanning the species table @@ -108,7 +133,6 @@ Future configureDependencies() async { ..registerSingleton(labelExtractor) ..registerSingleton(OnboardingStore(secretStore)) ..registerSingleton(LocaleStore(secretStore)) - ..registerSingleton(socialService) ..registerSingleton(SocialSettings(secretStore)) ..registerSingleton(OfferOutbox(secretStore)) ..registerSingleton(MessageStore(secretStore)) @@ -139,6 +163,17 @@ Future configureDependencies() async { ), ); } + + // Optional: absent only if the derivation above failed — then the app runs + // inventory-only, by design (market/chat/profile hidden). + if (socialService != null) { + getIt.registerSingleton(socialService); + } + + // Registered LAST: the "fully wired" sentinel the guard above checks. Anything + // that throws before here leaves the marker unregistered, so the next run + // rebuilds cleanly instead of reusing a half-built container. + getIt.registerSingleton<_DepsReady>(const _DepsReady()); } Future _backupsDir() async { diff --git a/apps/app_seeds/lib/main.dart b/apps/app_seeds/lib/main.dart index 977c9f7..2f82f29 100644 --- a/apps/app_seeds/lib/main.dart +++ b/apps/app_seeds/lib/main.dart @@ -32,7 +32,9 @@ Future main() async { repository: getIt(), species: getIt(), onboarding: onboarding, - social: getIt(), + social: getIt.isRegistered() + ? getIt() + : null, socialSettings: getIt(), location: const GeolocatorCoarseLocation(), outbox: getIt(),