Skip to content
Open
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
10 changes: 7 additions & 3 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ class _AppState extends State<App> {

final featureAccess = context.watch<FeatureAccess>();
final themeSettings = context.watch<ThemeSettings>();
// Optional read-only override from a host (the configurator's preview);
// null in a standalone run. When set it wins, so the preview reflects the
// host's light/dark toggle without persisting anything.
final hostThemeMode = context.watch<ThemeMode?>();

final materialApp = ThemeProvider(
settings: themeSettings,
Expand All @@ -128,9 +132,9 @@ class _AppState extends State<App> {
builder: (context, state) {
final themeProvider = ThemeProvider.of(context);
final forcedMode = featureAccess.supportedConfig.themeMode;
final finalThemeMode = forcedMode == ThemeMode.system
? themeSettings.effectiveThemeMode(state.themeMode)
: forcedMode;
final finalThemeMode =
hostThemeMode ??
(forcedMode == ThemeMode.system ? themeSettings.effectiveThemeMode(state.themeMode) : forcedMode);

return MaterialApp.router(
locale: state.effectiveLocale,
Expand Down
68 changes: 58 additions & 10 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,19 @@ void main() {

Logger.root.onRecord.listen(_onRootLogRecord);

runApp(RootApp(instanceRegistry: instanceRegistry));
runApp(
RootApp(
instanceRegistry: instanceRegistry,
featureAccess: (
initial: instanceRegistry.get<FeatureAccess>(),
updates: instanceRegistry.get<FeatureAccessStreamFactory>().create(),
),
themeSettings: (
initial: instanceRegistry.get<AppThemes>().values.first.settings,
updates: const Stream.empty(),
),
),
);
},
(error, stackTrace) {
logger.severe('runZonedGuarded', error, stackTrace);
Expand All @@ -65,33 +77,69 @@ void _onRootLogRecord(LogRecord record) {
}
}

/// A reactive config input: the [initial] value for the first frame plus an
/// [updates] stream that replaces it as it changes.
typedef ConfigSource<T> = ({T initial, Stream<T> updates});

class RootApp extends StatelessWidget {
const RootApp({super.key, required this.instanceRegistry});
const RootApp({
super.key,
required this.instanceRegistry,
required this.featureAccess,
required this.themeSettings,
this.themeMode,
});

final InstanceRegistry instanceRegistry;

/// Reactive config the app renders, provided down the tree as inherited values.
/// The composition root decides the source: standalone (`main`) resolves it
/// from the bootstrap registry; a host that embeds this app (the configurator's
/// realtime preview) passes its own streams so the preview reflects live edits.
/// RootApp itself stays agnostic - it only wires whatever source it is given.
final ConfigSource<FeatureAccess> featureAccess;
final ConfigSource<ThemeSettings> themeSettings;

/// Optional read-only override for the displayed theme mode (the configurator's
/// light/dark preview toggle). Null in a standalone run, where the mode comes
/// from FeatureAccess / AppState; when set it wins, and nothing is persisted.
final ConfigSource<ThemeMode>? themeMode;

@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<AppInfo>(create: (_) => instanceRegistry.get()),
// The active theme, provided down the tree as an inherited value so the
// app consumes it directly (see App.build) instead of holding it in
// AppState. A host that embeds this app can override this provider to
// drive the theme; standalone it is the first bootstrap-built theme.
Provider<ThemeSettings>(create: (_) => instanceRegistry.get<AppThemes>().values.first.settings),
// AppState. The source is supplied by the caller (see [themeSettings]).
StreamProvider<ThemeSettings>(
initialData: themeSettings.initial,
create: (_) => themeSettings.updates,
updateShouldNotify: (previous, next) => previous != next,
),
// Optional host theme-mode override (see [themeMode]); always provided as
// a nullable value so App can read it, null in a standalone run.
if (themeMode case final source?)
StreamProvider<ThemeMode?>(
initialData: source.initial,
create: (_) => source.updates,
updateShouldNotify: (previous, next) => previous != next,
)
else
Provider<ThemeMode?>.value(value: null),
Provider<PackageInfo>(create: (_) => instanceRegistry.get()),
// Stateless version-compatibility policy shared by the login gate and the
// in-app force-update gate; const, so no bootstrap registration needed.
Provider<AppCompatibilityResolver>(create: (_) => const DefaultAppCompatibilityResolver()),
Provider<DeviceInfo>(create: (_) => instanceRegistry.get()),
Provider<AppPreferences>(create: (_) => instanceRegistry.get()),
// Provides reactive [FeatureAccess] configuration synchronized with [SystemInfoRepository] and [RemoteConfigService].
//
// Initializes with bootstrap data and updates whenever system information or remote configuration changes.
// Reactive [FeatureAccess]; the source is supplied by the caller (see
// [featureAccess]). Standalone it is the bootstrap stream synchronized
// with SystemInfoRepository and RemoteConfigService.
StreamProvider<FeatureAccess>(
initialData: instanceRegistry.get<FeatureAccess>(),
create: (_) => instanceRegistry.get<FeatureAccessStreamFactory>().create(),
initialData: featureAccess.initial,
create: (_) => featureAccess.updates,
updateShouldNotify: (previous, next) => previous != next,
),
Provider<SecureStorage>(create: (_) => instanceRegistry.get()),
Expand Down
8 changes: 3 additions & 5 deletions screenshots/lib/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:webtrit_phone/data/data.dart';
import 'package:webtrit_phone/models/models.dart';
import 'package:webtrit_phone/repositories/repositories.dart';
import 'package:webtrit_phone/services/services.dart';
import 'package:webtrit_phone/theme/theme.dart';
import 'package:webtrit_phone/utils/utils.dart';

import 'package:screenshots/mocks/mocks.dart';
Expand Down Expand Up @@ -49,13 +50,10 @@ Future<AppContext> bootstrap() async {
featureAccess,
);

final appBloc = MockAppBloc.allScreen(
themeSettings: appThemes.values.first.settings,
themeMode: ThemeMode.light,
locale: const Locale('en'),
);
final appBloc = MockAppBloc.allScreen(themeMode: ThemeMode.light, locale: const Locale('en'));

final providers = [
Provider<ThemeSettings>.value(value: appThemes.values.first.settings),
Provider<FeatureAccess>.value(value: featureAccess),
Provider<PackageInfo>.value(value: packageInfo),
Provider<DeviceInfo>.value(value: deviceInfo),
Expand Down
8 changes: 1 addition & 7 deletions screenshots/lib/mocks/mock_app_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,16 @@ import 'package:bloc_test/bloc_test.dart';

import 'package:webtrit_phone/blocs/blocs.dart';
import 'package:webtrit_phone/models/agreement_status.dart';
import 'package:webtrit_phone/theme/theme.dart';

class MockAppBloc extends MockBloc<AppEvent, AppState> implements AppBloc {
MockAppBloc();

factory MockAppBloc.allScreen({
required ThemeSettings themeSettings,
required ThemeMode themeMode,
required Locale locale,
}) {
factory MockAppBloc.allScreen({required ThemeMode themeMode, required Locale locale}) {
final mock = MockAppBloc();
whenListen(
mock,
const Stream<AppState>.empty(),
initialState: AppState(
themeSettings: themeSettings,
themeMode: themeMode,
locale: locale,
userAgreementStatus: AgreementStatus.pending,
Expand Down
74 changes: 35 additions & 39 deletions screenshots/lib/widgets/screenshot_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:mocktail/mocktail.dart';

import 'package:webtrit_phone/blocs/app/app_bloc.dart';
import 'package:webtrit_phone/environment_config.dart';
import 'package:webtrit_phone/extensions/extensions.dart';
import 'package:webtrit_phone/l10n/l10n.dart';
import 'package:webtrit_phone/theme/theme.dart';

Expand All @@ -20,46 +21,41 @@ class ScreenshotApp extends StatelessWidget {

@override
Widget build(BuildContext context) {
Widget widgetsApp = BlocBuilder<AppBloc, AppState>(
buildWhen: (previous, current) => previous.themeSettings != current.themeSettings,
builder: (context, state) {
return ThemeProvider(
settings: state.themeSettings,
lightDynamic: null,
darkDynamic: null,
child: BlocBuilder<AppBloc, AppState>(
buildWhen: (previous, current) =>
previous.effectiveLocale != current.effectiveLocale ||
previous.effectiveThemeMode != current.effectiveThemeMode,
builder: (context, state) {
final themeProvider = ThemeProvider.of(context);

final effectiveThemeMode = state.effectiveThemeMode;
final ThemeData themeData;

if (effectiveThemeMode == ThemeMode.dark) {
themeData = themeProvider.dark();
} else {
themeData = themeProvider.light();
}

return WidgetsApp.router(
locale: state.effectiveLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
title: EnvironmentConfig.APP_NAME,
color: themeData.primaryColor,
debugShowCheckedModeBanner: false,
routerDelegate: ScreenshotRouterDelegate(child),
routeInformationParser: const _NoOpRouteInformationParser(),
builder: (context, child) {
return Theme(data: themeData, child: child!);
},
);
final themeSettings = context.watch<ThemeSettings>();
Widget widgetsApp = ThemeProvider(
settings: themeSettings,
lightDynamic: null,
darkDynamic: null,
child: BlocBuilder<AppBloc, AppState>(
buildWhen: (previous, current) =>
previous.effectiveLocale != current.effectiveLocale || previous.themeMode != current.themeMode,
builder: (context, state) {
final themeProvider = ThemeProvider.of(context);

final effectiveThemeMode = themeSettings.effectiveThemeMode(state.themeMode);
final ThemeData themeData;

if (effectiveThemeMode == ThemeMode.dark) {
themeData = themeProvider.dark();
} else {
themeData = themeProvider.light();
}

return WidgetsApp.router(
locale: state.effectiveLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
title: EnvironmentConfig.APP_NAME,
color: themeData.primaryColor,
debugShowCheckedModeBanner: false,
routerDelegate: ScreenshotRouterDelegate(child),
routeInformationParser: const _NoOpRouteInformationParser(),
builder: (context, child) {
return Theme(data: themeData, child: child!);
},
),
);
},
);
},
),
);

if (ignorePointer) {
Expand Down
Loading