diff --git a/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart b/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart index 5ce6c2e7d..c05c8b1cf 100644 --- a/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart +++ b/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart @@ -488,6 +488,15 @@ class CdnDefinitionProvider extends DefinitionProvider { } } + /// Whether a CDN artifact refresh should update appBundle and fire UI events + /// immediately. Background/inactive lifecycle must defer until [resumed] so + /// [_handlePendingUpdate] runs when a BuildContext exists for i18n refresh. + @visibleForTesting + bool shouldDeliverArtifactRefreshImmediately() => + cdnShouldDeliverArtifactRefreshImmediately( + WidgetsBinding.instance.lifecycleState, + ); + /// Check for updates and update cache if available /// Sets _hasPendingUpdate flag if updates were fetched Future _refreshIfStale() async { @@ -513,15 +522,16 @@ class CdnDefinitionProvider extends DefinitionProvider { // Save to persistent cache await _saveCachedState(jsonString); - // If artifact refresh is enabled and app is already initialized, - // immediately update appBundle and fire refresh event. - // This handles the cold start scenario where background refresh - // completes after initial render. - if (isArtifactRefreshEnabled() && Ensemble().getConfig() != null) { - await _handlePendingUpdate(); - } else { - // Mark for later refresh on next resume - _hasPendingUpdate = true; + // Foreground/cold-start: sync appBundle + translations before refresh event. + // Background: defer until resume (see onAppLifecycleStateChanged) so i18n + // refresh is not skipped when context is unavailable. + if (isArtifactRefreshEnabled()) { + if (shouldDeliverArtifactRefreshImmediately() && + Ensemble().getConfig() != null) { + await _handlePendingUpdate(); + } else { + _hasPendingUpdate = true; + } } } catch (e) { if (kDebugMode) { @@ -947,3 +957,9 @@ class CdnDefinitionProvider extends DefinitionProvider { } } } + +/// True when CDN artifact refresh events may be delivered immediately. +@visibleForTesting +bool cdnShouldDeliverArtifactRefreshImmediately(AppLifecycleState? state) { + return state == null || state == AppLifecycleState.resumed; +} diff --git a/modules/ensemble/test/cdn_provider_test.dart b/modules/ensemble/test/cdn_provider_test.dart index 97d3df78d..5550df70a 100644 --- a/modules/ensemble/test/cdn_provider_test.dart +++ b/modules/ensemble/test/cdn_provider_test.dart @@ -9,6 +9,35 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { + group('cdnShouldDeliverArtifactRefreshImmediately', () { + test('allows delivery when lifecycle is unknown or resumed', () { + expect(cdnShouldDeliverArtifactRefreshImmediately(null), isTrue); + expect( + cdnShouldDeliverArtifactRefreshImmediately(AppLifecycleState.resumed), + isTrue, + ); + }); + + test('defers delivery while app is backgrounded or inactive', () { + expect( + cdnShouldDeliverArtifactRefreshImmediately(AppLifecycleState.paused), + isFalse, + ); + expect( + cdnShouldDeliverArtifactRefreshImmediately(AppLifecycleState.inactive), + isFalse, + ); + expect( + cdnShouldDeliverArtifactRefreshImmediately(AppLifecycleState.hidden), + isFalse, + ); + expect( + cdnShouldDeliverArtifactRefreshImmediately(AppLifecycleState.detached), + isFalse, + ); + }); + }); + group('CDN cache invalidation', () { test('resets freshness metadata when persisted manifest is invalid', () async {