neoagent 2.3.1-beta.62 → 2.3.1-beta.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -51,6 +51,15 @@ NEOAGENT_RELEASE_CHANNEL=stable
51
51
  # Example: http://localhost:3333,https://yourdomain.com
52
52
  ALLOWED_ORIGINS=
53
53
 
54
+ ########################################
55
+ # Analytics
56
+ ########################################
57
+
58
+ # Mixpanel project token used by the Flutter app.
59
+ # Leave this blank to disable analytics, or set your own public project token.
60
+ # Keep the token below if you want to support the development of NeoAgent with anonymous usage analytics.
61
+ NEOAGENT_MIXPANEL_TOKEN=4a47ae6a05cf39a8faf0438a1200dde6
62
+
54
63
  ########################################
55
64
  # Service email
56
65
  ########################################
@@ -18,6 +18,17 @@ AI provider credentials, OAuth client secrets, and deployment controls are not c
18
18
  | `NEOAGENT_DEPLOYMENT_MODE` | `self_hosted` | `self_hosted` enables in-app update controls; `managed` hides operator-only controls for SaaS deployments. |
19
19
  | `NEOAGENT_RELEASE_CHANNEL` | `stable` | Release track used by the self-hosted updater. |
20
20
 
21
+ ## Analytics
22
+
23
+ NeoAgent can send anonymous Mixpanel usage events from the Flutter client. The events cover app startup, section navigation, auth success, backend setup, onboarding dismissal, chat sends, recording start/stop, task runs, update checks, and update triggers. They do not include message content, credentials, or other app data.
24
+
25
+ Set `NEOAGENT_MIXPANEL_TOKEN` to your own public Mixpanel project token to enable analytics. Leave it blank to disable analytics entirely.
26
+ When analytics is enabled, the web client shows a dismissible cookie-consent banner until the user accepts or declines.
27
+
28
+ | Variable | Default | Description |
29
+ |---|---:|---|
30
+ | `NEOAGENT_MIXPANEL_TOKEN` | optional | Public Mixpanel project token used by the Flutter client. |
31
+
21
32
  ## Service Email
22
33
 
23
34
  Service email is optional. When `NEOAGENT_EMAIL_FROM` and `NEOAGENT_EMAIL_SMTP_HOST` are set, NeoAgent uses SMTP for account security flows: signup confirmation, password reset, unusual login notifications, password change notifications, and email change notifications. Confirmation and reset links use the same `PUBLIC_URL` base as the other server-generated links.
@@ -26,6 +26,7 @@ import 'src/android_apk_drop_zone.dart';
26
26
  import 'src/android_launcher_bridge.dart';
27
27
  import 'src/app_launch_bridge.dart';
28
28
  import 'src/app_release_updater.dart' as app_release_updater;
29
+ import 'src/analytics_service.dart';
29
30
  import 'src/backend_client.dart';
30
31
  import 'src/desktop_companion.dart';
31
32
  import 'src/desktop_screen_capture.dart';
@@ -37,6 +37,7 @@ class NeoAgentController extends ChangeNotifier {
37
37
  final OAuthLauncher _oauthLauncher;
38
38
  final app_release_updater.AppReleaseUpdater _appReleaseUpdater =
39
39
  app_release_updater.AppReleaseUpdater();
40
+ final AppAnalytics _analytics = AppAnalytics();
40
41
  final LiveVoiceCapture _liveVoiceCapture = LiveVoiceCapture();
41
42
  final DesktopScreenCapture _desktopScreenCapture =
42
43
  createDesktopScreenCapture();
@@ -98,6 +99,10 @@ class NeoAgentController extends ChangeNotifier {
98
99
  bool networkStatusKnown = false;
99
100
  bool _desktopFloatingToolbarPopupRequested = false;
100
101
  bool _voiceAssistantIncludeScreenContext = false;
102
+ bool _didTrackAppOpen = false;
103
+ bool _analyticsConfigured = false;
104
+ bool _analyticsConsentResolved = false;
105
+ bool _analyticsConsentGranted = false;
101
106
 
102
107
  bool hasUser = true;
103
108
  bool registrationOpen = false;
@@ -319,6 +324,7 @@ class NeoAgentController extends ChangeNotifier {
319
324
  _diagnosticLogSubscription?.cancel();
320
325
  _connectivitySubscription?.cancel();
321
326
  _appReleaseUpdater.dispose();
327
+ unawaited(_analytics.dispose());
322
328
  _desktopCompanion.removeListener(notifyListeners);
323
329
  unawaited(_desktopCompanion.disconnect());
324
330
  _recordingBridge.removeListener(_handleRecordingBridgeChanged);
@@ -349,6 +355,12 @@ class NeoAgentController extends ChangeNotifier {
349
355
  bool get canCaptureVoiceAssistantScreenContext =>
350
356
  _desktopScreenCapture.isSupported;
351
357
 
358
+ bool get showAnalyticsConsentBanner =>
359
+ kIsWeb && _analyticsConfigured && !_analyticsConsentResolved;
360
+
361
+ bool get analyticsConsentGranted =>
362
+ _analyticsConsentResolved && _analyticsConsentGranted;
363
+
352
364
  bool get isLiveVoiceCaptureEngaged =>
353
365
  _isStartingLiveVoice || _liveVoiceCaptureActive;
354
366
 
@@ -701,6 +713,7 @@ class NeoAgentController extends ChangeNotifier {
701
713
  appUpdateAutoCheckEnabled =
702
714
  _prefs?.getBool('app.update.autoCheckEnabled') ?? true;
703
715
  installedAppVersion = await _safeLoadInstalledAppVersion();
716
+ await _initializeAnalytics();
704
717
  await refreshConnectivityStatus();
705
718
  if (_connectivityPluginAvailable && _connectivitySubscription == null) {
706
719
  try {
@@ -862,6 +875,101 @@ class NeoAgentController extends ChangeNotifier {
862
875
  }
863
876
  }
864
877
 
878
+ Future<void> _initializeAnalytics() async {
879
+ try {
880
+ final runtimeConfig = await _backendClient.fetchRuntimeConfig(backendUrl);
881
+ final analyticsConfig = runtimeConfig['analytics'];
882
+ final mixpanelConfig = analyticsConfig is Map
883
+ ? analyticsConfig['mixpanel']
884
+ : null;
885
+ final token = mixpanelConfig is Map
886
+ ? mixpanelConfig['token']?.toString()
887
+ : null;
888
+ _analyticsConfigured = token != null && token.trim().isNotEmpty;
889
+ final consentState = _prefs?.getBool('analytics.cookieConsent');
890
+ _analyticsConsentResolved = consentState != null;
891
+ _analyticsConsentGranted = consentState == true;
892
+ await _analytics.initialize(
893
+ token: token,
894
+ consentGranted: _analyticsConsentGranted,
895
+ );
896
+ _trackAppOpenedIfNeeded();
897
+ } catch (_) {
898
+ _analyticsConfigured = false;
899
+ _analyticsConsentResolved = false;
900
+ _analyticsConsentGranted = false;
901
+ await _analytics.initialize(token: null, consentGranted: false);
902
+ }
903
+ }
904
+
905
+ void _trackAppOpenedIfNeeded({String? consentSource}) {
906
+ if (!_analytics.enabled || _didTrackAppOpen) {
907
+ return;
908
+ }
909
+ _didTrackAppOpen = true;
910
+ unawaited(
911
+ _analytics.trackAppOpened(
912
+ appMode: appMode.name,
913
+ platform: _analyticsPlatformLabel(),
914
+ backendMode: backendUrl.trim().isEmpty ? 'same_origin' : 'custom',
915
+ selectedSection: selectedSection.label,
916
+ deploymentProfile: deploymentProfile,
917
+ authenticated: isAuthenticated,
918
+ ),
919
+ );
920
+ if (consentSource != null) {
921
+ unawaited(
922
+ _analytics.track(
923
+ 'analytics_consent_granted',
924
+ properties: <String, Object?>{'consent_source': consentSource},
925
+ ),
926
+ );
927
+ }
928
+ }
929
+
930
+ String _analyticsPlatformLabel() {
931
+ if (kIsWeb) {
932
+ return 'web';
933
+ }
934
+ switch (defaultTargetPlatform) {
935
+ case TargetPlatform.android:
936
+ return 'android';
937
+ case TargetPlatform.iOS:
938
+ return 'ios';
939
+ case TargetPlatform.macOS:
940
+ return 'macos';
941
+ case TargetPlatform.windows:
942
+ return 'windows';
943
+ case TargetPlatform.linux:
944
+ return 'linux';
945
+ case TargetPlatform.fuchsia:
946
+ return 'fuchsia';
947
+ }
948
+ }
949
+
950
+ Future<void> acceptAnalyticsConsent() async {
951
+ if (!_analyticsConfigured) {
952
+ return;
953
+ }
954
+ _analyticsConsentResolved = true;
955
+ _analyticsConsentGranted = true;
956
+ await _prefs?.setBool('analytics.cookieConsent', true);
957
+ await _analytics.setConsentGranted(true);
958
+ _trackAppOpenedIfNeeded(consentSource: 'banner');
959
+ notifyListeners();
960
+ }
961
+
962
+ Future<void> declineAnalyticsConsent() async {
963
+ if (!_analyticsConfigured) {
964
+ return;
965
+ }
966
+ _analyticsConsentResolved = true;
967
+ _analyticsConsentGranted = false;
968
+ await _prefs?.setBool('analytics.cookieConsent', false);
969
+ await _analytics.setConsentGranted(false);
970
+ notifyListeners();
971
+ }
972
+
865
973
  Future<void> setAppUpdateChannel(String channel) async {
866
974
  final normalized = channel.trim().toLowerCase() == 'beta'
867
975
  ? 'beta'
@@ -886,6 +994,7 @@ class NeoAgentController extends ChangeNotifier {
886
994
  if (isCheckingAppUpdate) {
887
995
  return;
888
996
  }
997
+ unawaited(_analytics.trackAppUpdateCheck(silent: silent));
889
998
  if (!appUpdaterConfigured) {
890
999
  appUpdateErrorMessage = kIsWeb
891
1000
  ? 'Client app update checks are unavailable in the web app.'
@@ -975,6 +1084,11 @@ class NeoAgentController extends ChangeNotifier {
975
1084
  isBooting = true;
976
1085
  notifyListeners();
977
1086
  await bootstrap();
1087
+ unawaited(
1088
+ _analytics.trackBackendUrlSaved(
1089
+ backendMode: backendUrl.trim().isEmpty ? 'same_origin' : 'custom',
1090
+ ),
1091
+ );
978
1092
  return true;
979
1093
  } catch (error) {
980
1094
  errorMessage = _friendlyErrorMessage(error);
@@ -1147,6 +1261,7 @@ class NeoAgentController extends ChangeNotifier {
1147
1261
  response,
1148
1262
  retentionErrorMessage:
1149
1263
  'QR login completed, but NeoAgent could not keep the session. Please try again.',
1264
+ authMethod: 'qr',
1150
1265
  );
1151
1266
  } catch (error) {
1152
1267
  final message = _friendlyErrorMessage(error);
@@ -1199,6 +1314,7 @@ class NeoAgentController extends ChangeNotifier {
1199
1314
  String? fallbackUsername,
1200
1315
  String? retentionErrorMessage,
1201
1316
  bool isRegistration = false,
1317
+ String authMethod = 'password',
1202
1318
  }) async {
1203
1319
  user = Map<String, dynamic>.from(
1204
1320
  response['user'] as Map<dynamic, dynamic>? ??
@@ -1220,6 +1336,14 @@ class NeoAgentController extends ChangeNotifier {
1220
1336
  _clearQrLoginChallenge();
1221
1337
  await _persistCredentials();
1222
1338
  await refresh();
1339
+ if (isAuthenticated) {
1340
+ unawaited(
1341
+ _analytics.trackSignedIn(
1342
+ authMethod: authMethod,
1343
+ isRegistration: isRegistration,
1344
+ ),
1345
+ );
1346
+ }
1223
1347
  if (!isAuthenticated && retentionErrorMessage != null) {
1224
1348
  errorMessage = retentionErrorMessage;
1225
1349
  }
@@ -1289,6 +1413,7 @@ class NeoAgentController extends ChangeNotifier {
1289
1413
  await _completeAuthenticatedResponse(
1290
1414
  response,
1291
1415
  isRegistration: register,
1416
+ authMethod: 'oauth',
1292
1417
  retentionErrorMessage:
1293
1418
  'Sign-in completed, but NeoAgent could not keep the browser session. Please sign in again. If this keeps happening, check backend session cookie settings.',
1294
1419
  );
@@ -1315,6 +1440,7 @@ class NeoAgentController extends ChangeNotifier {
1315
1440
  await _completeAuthenticatedResponse(
1316
1441
  response,
1317
1442
  fallbackUsername: pendingTwoFactorUsername,
1443
+ authMethod: 'two_factor',
1318
1444
  retentionErrorMessage:
1319
1445
  'Two-factor sign-in completed, but NeoAgent could not keep the browser session. Please sign in again.',
1320
1446
  );
@@ -1405,6 +1531,7 @@ class NeoAgentController extends ChangeNotifier {
1405
1531
  response,
1406
1532
  fallbackUsername: username,
1407
1533
  isRegistration: register,
1534
+ authMethod: 'password',
1408
1535
  retentionErrorMessage:
1409
1536
  'Sign-in completed, but NeoAgent could not keep the browser session. Please sign in again. If this keeps happening, the backend session cookie is likely not being retained.',
1410
1537
  );
@@ -1435,6 +1562,7 @@ class NeoAgentController extends ChangeNotifier {
1435
1562
  final logoutFuture = _backendClient.logout(backendUrl);
1436
1563
  _authCycle += 1;
1437
1564
  _clearAuthenticatedState();
1565
+ unawaited(_analytics.trackSignedOut());
1438
1566
  isAuthenticating = true;
1439
1567
  notifyListeners();
1440
1568
 
@@ -1451,6 +1579,7 @@ class NeoAgentController extends ChangeNotifier {
1451
1579
  notifyListeners();
1452
1580
  try {
1453
1581
  await _backendClient.completeOnboarding(backendUrl);
1582
+ unawaited(_analytics.trackOnboardingDismissed());
1454
1583
  if (isAuthenticated && user != null) {
1455
1584
  user!['hasCompletedOnboarding'] = true;
1456
1585
  }
@@ -1604,6 +1733,7 @@ class NeoAgentController extends ChangeNotifier {
1604
1733
  }
1605
1734
 
1606
1735
  void setSelectedSection(AppSection section) {
1736
+ final previousSection = selectedSection;
1607
1737
  selectedSection = section;
1608
1738
  unawaited(_prefs?.setString(_selectedSectionPrefsKey, section.name));
1609
1739
  if (section == AppSection.devices) {
@@ -1612,6 +1742,14 @@ class NeoAgentController extends ChangeNotifier {
1612
1742
  if (section == AppSection.accountSettings) {
1613
1743
  unawaited(refreshAccountSettings());
1614
1744
  }
1745
+ if (previousSection != section) {
1746
+ unawaited(
1747
+ _analytics.trackSectionChanged(
1748
+ section: section.label,
1749
+ previousSection: previousSection.label,
1750
+ ),
1751
+ );
1752
+ }
1615
1753
  notifyListeners();
1616
1754
  }
1617
1755
 
@@ -2422,6 +2560,9 @@ class NeoAgentController extends ChangeNotifier {
2422
2560
  }
2423
2561
 
2424
2562
  Future<void> refreshWidgets({bool all = false}) async {
2563
+ if (all) {
2564
+ unawaited(_analytics.trackWidgetRefreshRequested(all: all));
2565
+ }
2425
2566
  widgets = _decodeModelList(
2426
2567
  'widgets',
2427
2568
  await _backendClient.fetchWidgets(
@@ -3224,6 +3365,7 @@ class NeoAgentController extends ChangeNotifier {
3224
3365
  }
3225
3366
 
3226
3367
  Future<void> startBackgroundRecording() async {
3368
+ unawaited(_analytics.trackRecordingStarted(kind: 'background'));
3227
3369
  await _startRecordingCapture(
3228
3370
  logKey: 'start_background',
3229
3371
  payload: buildAndroidBackgroundRecordingPayload(),
@@ -3250,6 +3392,7 @@ class NeoAgentController extends ChangeNotifier {
3250
3392
  }
3251
3393
  return;
3252
3394
  }
3395
+ unawaited(_analytics.trackRecordingStarted(kind: 'desktop'));
3253
3396
  await _startRecordingCapture(
3254
3397
  logKey: 'start_desktop',
3255
3398
  payload: buildDesktopRecordingPayload(),
@@ -3633,6 +3776,15 @@ class NeoAgentController extends ChangeNotifier {
3633
3776
  notifyListeners();
3634
3777
  try {
3635
3778
  _desktopFloatingToolbarPopupRequested = false;
3779
+ unawaited(
3780
+ _analytics.trackRecordingStopped(
3781
+ kind: recordingRuntime.supportsBackgroundMic &&
3782
+ !recordingRuntime.supportsScreenAndMic
3783
+ ? 'background'
3784
+ : 'desktop',
3785
+ stopReason: stopReason,
3786
+ ),
3787
+ );
3636
3788
  await _recordingBridge.stopActiveRecording();
3637
3789
  if (isAndroidBackgroundStop) {
3638
3790
  await Future<void>.delayed(const Duration(milliseconds: 600));
@@ -4261,6 +4413,12 @@ class NeoAgentController extends ChangeNotifier {
4261
4413
  if (trimmed.isEmpty || (isSendingMessage && !canSteerLiveRun)) {
4262
4414
  return;
4263
4415
  }
4416
+ unawaited(
4417
+ _analytics.trackChatMessageSent(
4418
+ length: trimmed.length,
4419
+ steeringLiveRun: canSteerLiveRun,
4420
+ ),
4421
+ );
4264
4422
 
4265
4423
  final optimistic = ChatEntry(
4266
4424
  id: '',
@@ -4682,6 +4840,7 @@ class NeoAgentController extends ChangeNotifier {
4682
4840
  errorMessage = null;
4683
4841
  notifyListeners();
4684
4842
  try {
4843
+ unawaited(_analytics.trackAppUpdateTriggered());
4685
4844
  await _backendClient.triggerUpdate(backendUrl);
4686
4845
  await refreshUpdateStatus();
4687
4846
  } catch (error) {
@@ -5412,8 +5571,7 @@ class NeoAgentController extends ChangeNotifier {
5412
5571
  return;
5413
5572
  }
5414
5573
  _pendingChatDraft = normalized;
5415
- selectedSection = AppSection.chat;
5416
- notifyListeners();
5574
+ setSelectedSection(AppSection.chat);
5417
5575
  }
5418
5576
 
5419
5577
  String? takePendingChatDraft() {
@@ -5432,17 +5590,15 @@ class NeoAgentController extends ChangeNotifier {
5432
5590
 
5433
5591
  void openWidgetSurface(String widgetId) {
5434
5592
  final normalized = widgetId.trim();
5435
- selectedSection = AppSection.widgets;
5593
+ setSelectedSection(AppSection.widgets);
5436
5594
  if (normalized.isNotEmpty) {
5437
5595
  _selectedWidgetId = normalized;
5438
5596
  unawaited(refreshWidgets(all: true));
5439
5597
  }
5440
- notifyListeners();
5441
5598
  }
5442
5599
 
5443
5600
  void openVoiceAssistantSurface() {
5444
- selectedSection = AppSection.voiceAssistant;
5445
- notifyListeners();
5601
+ setSelectedSection(AppSection.voiceAssistant);
5446
5602
  }
5447
5603
 
5448
5604
  String? get selectedWidgetId => _selectedWidgetId;
@@ -5475,6 +5631,7 @@ class NeoAgentController extends ChangeNotifier {
5475
5631
  return;
5476
5632
  }
5477
5633
  _startManualRunCooldown('task', '$id');
5634
+ unawaited(_analytics.trackTaskRunRequested(taskId: id));
5478
5635
  await _backendClient.runSavedTask(backendUrl, id);
5479
5636
  await refreshTasks();
5480
5637
  await refreshRunsOnly();
@@ -627,6 +627,28 @@ class _NeoAgentAppState extends State<NeoAgentApp>
627
627
  ),
628
628
  ),
629
629
  ),
630
+ if (!_desktopToolbarWindowMode &&
631
+ !_desktopAssistantPopupWindowMode &&
632
+ _controller.showAnalyticsConsentBanner)
633
+ Positioned(
634
+ left: 0,
635
+ right: 0,
636
+ bottom: 0,
637
+ child: SafeArea(
638
+ top: false,
639
+ child: Padding(
640
+ padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
641
+ child: Center(
642
+ child: ConstrainedBox(
643
+ constraints: const BoxConstraints(maxWidth: 980),
644
+ child: _GlobalAnalyticsConsentBanner(
645
+ controller: _controller,
646
+ ),
647
+ ),
648
+ ),
649
+ ),
650
+ ),
651
+ ),
630
652
  if (_supportsDesktopShell &&
631
653
  !_desktopToolbarWindowMode &&
632
654
  !_desktopAssistantPopupWindowMode)
@@ -195,11 +195,21 @@ class _SettingsPanelState extends State<SettingsPanel> {
195
195
  final modelChoices = <DropdownMenuItem<String>>[
196
196
  const DropdownMenuItem<String>(
197
197
  value: 'auto',
198
- child: Text('Smart Selector (Auto)'),
198
+ child: Text(
199
+ 'Smart Selector (Auto)',
200
+ maxLines: 1,
201
+ overflow: TextOverflow.ellipsis,
202
+ ),
199
203
  ),
200
204
  ...routingModels.map(
201
- (model) =>
202
- DropdownMenuItem<String>(value: model.id, child: Text(model.label)),
205
+ (model) => DropdownMenuItem<String>(
206
+ value: model.id,
207
+ child: Text(
208
+ model.label,
209
+ maxLines: 1,
210
+ overflow: TextOverflow.ellipsis,
211
+ ),
212
+ ),
203
213
  ),
204
214
  ];
205
215
  final enabledSmartModels = _enabledModels
@@ -634,7 +644,11 @@ class _SettingsPanelState extends State<SettingsPanel> {
634
644
  .map(
635
645
  (model) => DropdownMenuItem<String>(
636
646
  value: model.id,
637
- child: Text(model.label),
647
+ child: Text(
648
+ model.label,
649
+ maxLines: 1,
650
+ overflow: TextOverflow.ellipsis,
651
+ ),
638
652
  ),
639
653
  )
640
654
  .toList(),
@@ -2030,6 +2044,7 @@ class _RoutingSelectCard extends StatelessWidget {
2030
2044
  DropdownButtonFormField<String>(
2031
2045
  initialValue: value,
2032
2046
  items: items,
2047
+ isExpanded: true,
2033
2048
  decoration: const InputDecoration(isDense: true),
2034
2049
  onChanged: onChanged,
2035
2050
  ),
@@ -1826,6 +1826,167 @@ class _GlobalWebUpdateBanner extends StatelessWidget {
1826
1826
  }
1827
1827
  }
1828
1828
 
1829
+ class _GlobalAnalyticsConsentBanner extends StatelessWidget {
1830
+ const _GlobalAnalyticsConsentBanner({required this.controller});
1831
+
1832
+ final NeoAgentController controller;
1833
+
1834
+ @override
1835
+ Widget build(BuildContext context) {
1836
+ return LayoutBuilder(
1837
+ builder: (context, constraints) {
1838
+ final compact = constraints.maxWidth < 700;
1839
+ final body = Text(
1840
+ 'NeoAgent uses anonymous analytics to understand startup, navigation, and setup flows. No messages, credentials, or personal content are collected.',
1841
+ style: TextStyle(color: _textSecondary, height: 1.35),
1842
+ );
1843
+ return Material(
1844
+ color: Colors.transparent,
1845
+ child: _GlassSurface(
1846
+ borderRadius: BorderRadius.circular(22),
1847
+ blurSigma: 26,
1848
+ fillColor: _bgCard.withValues(alpha: 0.94),
1849
+ borderColor: _accent.withValues(alpha: 0.18),
1850
+ overlayGradient: LinearGradient(
1851
+ colors: <Color>[
1852
+ _accent.withValues(alpha: 0.12),
1853
+ _bgCard.withValues(alpha: 0.88),
1854
+ _bgSecondary.withValues(alpha: 0.86),
1855
+ ],
1856
+ stops: const <double>[0, 0.22, 1],
1857
+ begin: const Alignment(-1, -1),
1858
+ end: const Alignment(1, 1),
1859
+ ),
1860
+ boxShadow: <BoxShadow>[
1861
+ BoxShadow(
1862
+ color: Colors.black.withValues(alpha: 0.22),
1863
+ blurRadius: 34,
1864
+ offset: const Offset(0, 18),
1865
+ ),
1866
+ BoxShadow(
1867
+ color: _accent.withValues(alpha: 0.1),
1868
+ blurRadius: 24,
1869
+ offset: const Offset(0, 8),
1870
+ ),
1871
+ ],
1872
+ padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
1873
+ child: compact
1874
+ ? Column(
1875
+ crossAxisAlignment: CrossAxisAlignment.start,
1876
+ children: <Widget>[
1877
+ Row(
1878
+ crossAxisAlignment: CrossAxisAlignment.start,
1879
+ children: <Widget>[
1880
+ Container(
1881
+ padding: const EdgeInsets.all(10),
1882
+ decoration: BoxDecoration(
1883
+ color: _accent.withValues(alpha: 0.16),
1884
+ shape: BoxShape.circle,
1885
+ border: Border.all(
1886
+ color: _accent.withValues(alpha: 0.24),
1887
+ ),
1888
+ ),
1889
+ child: Icon(
1890
+ Icons.cookie_outlined,
1891
+ color: _accent,
1892
+ size: 22,
1893
+ ),
1894
+ ),
1895
+ const SizedBox(width: 12),
1896
+ Expanded(
1897
+ child: Column(
1898
+ crossAxisAlignment: CrossAxisAlignment.start,
1899
+ children: <Widget>[
1900
+ Text(
1901
+ 'Analytics cookies',
1902
+ style: TextStyle(
1903
+ color: _textPrimary,
1904
+ fontSize: 15,
1905
+ fontWeight: FontWeight.w700,
1906
+ ),
1907
+ ),
1908
+ const SizedBox(height: 6),
1909
+ body,
1910
+ ],
1911
+ ),
1912
+ ),
1913
+ ],
1914
+ ),
1915
+ const SizedBox(height: 14),
1916
+ Row(
1917
+ children: <Widget>[
1918
+ Expanded(
1919
+ child: OutlinedButton(
1920
+ onPressed: controller.declineAnalyticsConsent,
1921
+ child: const Text('Decline'),
1922
+ ),
1923
+ ),
1924
+ const SizedBox(width: 10),
1925
+ Expanded(
1926
+ child: FilledButton(
1927
+ onPressed: controller.acceptAnalyticsConsent,
1928
+ child: const Text('Allow analytics'),
1929
+ ),
1930
+ ),
1931
+ ],
1932
+ ),
1933
+ ],
1934
+ )
1935
+ : Row(
1936
+ crossAxisAlignment: CrossAxisAlignment.center,
1937
+ children: <Widget>[
1938
+ Container(
1939
+ padding: const EdgeInsets.all(10),
1940
+ decoration: BoxDecoration(
1941
+ color: _accent.withValues(alpha: 0.16),
1942
+ shape: BoxShape.circle,
1943
+ border: Border.all(
1944
+ color: _accent.withValues(alpha: 0.24),
1945
+ ),
1946
+ ),
1947
+ child: Icon(
1948
+ Icons.cookie_outlined,
1949
+ color: _accent,
1950
+ size: 22,
1951
+ ),
1952
+ ),
1953
+ const SizedBox(width: 14),
1954
+ Expanded(
1955
+ child: Column(
1956
+ crossAxisAlignment: CrossAxisAlignment.start,
1957
+ children: <Widget>[
1958
+ Text(
1959
+ 'Analytics cookies',
1960
+ style: TextStyle(
1961
+ color: _textPrimary,
1962
+ fontSize: 15,
1963
+ fontWeight: FontWeight.w700,
1964
+ ),
1965
+ ),
1966
+ const SizedBox(height: 6),
1967
+ body,
1968
+ ],
1969
+ ),
1970
+ ),
1971
+ const SizedBox(width: 16),
1972
+ OutlinedButton(
1973
+ onPressed: controller.declineAnalyticsConsent,
1974
+ child: const Text('Decline'),
1975
+ ),
1976
+ const SizedBox(width: 10),
1977
+ FilledButton(
1978
+ onPressed: controller.acceptAnalyticsConsent,
1979
+ child: const Text('Allow analytics'),
1980
+ ),
1981
+ ],
1982
+ ),
1983
+ ),
1984
+ );
1985
+ },
1986
+ );
1987
+ }
1988
+ }
1989
+
1829
1990
  class _DesktopCloseDecision {
1830
1991
  const _DesktopCloseDecision({
1831
1992
  required this.keepRunning,