Enable WASM experiment for Flutter beta branches (#9455)

diff --git a/packages/devtools_app/lib/src/shared/feature_flags.dart b/packages/devtools_app/lib/src/shared/feature_flags.dart
index d885a3e..b9a331c 100644
--- a/packages/devtools_app/lib/src/shared/feature_flags.dart
+++ b/packages/devtools_app/lib/src/shared/feature_flags.dart
@@ -110,6 +110,16 @@
     enabled: true,
   );
 
+  /// Flag to enable refactors in the Flutter Property Editor sidebar.
+  ///
+  /// https://github.com/flutter/devtools/issues/9214
+  static const wasmByDefault = FlutterChannelFeatureFlag(
+    name: 'wasmByDefault',
+    flutterChannel: FlutterChannel.beta,
+    enabledForDartApps: false,
+    enabledForFlutterAppsFallback: false,
+  );
+
   /// A set of all the boolean feature flags for debugging purposes.
   ///
   /// When adding a new boolean flag, you are responsible for adding it to this
@@ -130,7 +140,7 @@
   /// When adding a new Flutter channel flag, you are responsible for adding it
   /// to this map as well.
   static final _flutterChannelFlags = <FlutterChannelFeatureFlag>{
-    // TODO(https://github.com/flutter/devtools/issues/9438): Add wasm flag.
+    wasmByDefault,
   };
 
   /// A helper to print the status of all the feature flags.
diff --git a/packages/devtools_app/lib/src/shared/managers/banner_messages.dart b/packages/devtools_app/lib/src/shared/managers/banner_messages.dart
index c3fc64a..45e0b77 100644
--- a/packages/devtools_app/lib/src/shared/managers/banner_messages.dart
+++ b/packages/devtools_app/lib/src/shared/managers/banner_messages.dart
@@ -545,6 +545,39 @@
       );
 }
 
+class WasmWelcomeMessage extends BannerInfo {
+  WasmWelcomeMessage()
+    : super(
+        key: const Key('WasmWelcomeMessage'),
+        screenId: universalScreenId,
+        dismissOnConnectionChanges: true,
+        buildTextSpans: (context) => [
+          const TextSpan(
+            text:
+                '🚀 A faster and more performant DevTools is now available on WebAssembly! Click ',
+          ),
+          const TextSpan(
+            text: 'Enable',
+            style: TextStyle(fontWeight: FontWeight.bold),
+          ),
+          const TextSpan(text: ' to try it out now.'),
+          const TextSpan(
+            text: ' Please note that this will trigger a reload of DevTools.',
+            style: TextStyle(fontStyle: FontStyle.italic),
+          ),
+        ],
+        buildActions: (context) => [
+          DevToolsButton(
+            label: 'Enable',
+            onPressed: () async {
+              await preferences.enableWasmInStorage();
+              webReload();
+            },
+          ),
+        ],
+      );
+}
+
 void maybePushDebugModePerformanceMessage(String screenId) {
   if (offlineDataController.showingOfflineData.value) return;
   if (serviceConnection.serviceManager.connectedApp?.isDebugFlutterAppNow ??
@@ -577,6 +610,10 @@
   bannerMessages.addMessage(WelcomeToNewInspectorMessage(screenId: screenId));
 }
 
+void pushWasmWelcomeMessage() {
+  bannerMessages.addMessage(WasmWelcomeMessage());
+}
+
 extension BannerMessageThemeExtension on ThemeData {
   TextStyle get warningMessageLinkStyle => regularTextStyle.copyWith(
     decoration: TextDecoration.underline,
diff --git a/packages/devtools_app/lib/src/shared/preferences/preferences.dart b/packages/devtools_app/lib/src/shared/preferences/preferences.dart
index c54e260..32422f1 100644
--- a/packages/devtools_app/lib/src/shared/preferences/preferences.dart
+++ b/packages/devtools_app/lib/src/shared/preferences/preferences.dart
@@ -19,6 +19,7 @@
 import '../diagnostics/inspector_service.dart';
 import '../feature_flags.dart';
 import '../globals.dart';
+import '../managers/banner_messages.dart';
 import '../primitives/query_parameters.dart';
 import '../server/server.dart';
 import '../utils/utils.dart';
@@ -65,6 +66,8 @@
 /// A controller for global application preferences.
 class PreferencesController extends DisposableController
     with AutoDisposeControllerMixin {
+  static const _welcomeShownStorageId = 'wasmWelcomeShown';
+
   /// Whether the user preference for DevTools theme is set to dark mode.
   ///
   /// To check whether DevTools is using a light or dark theme, other parts of
@@ -134,6 +137,13 @@
     setGlobal(PreferencesController, this);
   }
 
+  /// Enables the wasm experiment in storage.
+  ///
+  /// This is used to persist the preference across reloads.
+  Future<void> enableWasmInStorage() async {
+    await storage.setValue(_ExperimentPreferences.wasm.storageKey, 'true');
+  }
+
   Future<void> _initDarkMode() async {
     final darkModeValue = await storage.getValue(
       _UiPreferences.darkMode.storageKey,
@@ -181,6 +191,18 @@
       );
     }
 
+    // Maybe show the WASM welcome message on app connection if this is the
+    // first time the user is loading DevTools after the WASM experiment was
+    // enabled.
+    addAutoDisposeListener(
+      serviceConnection.serviceManager.connectedState,
+      () async {
+        if (serviceConnection.serviceManager.connectedState.value.connected) {
+          await _maybeShowWasmWelcomeMessage();
+        }
+      },
+    );
+
     addAutoDisposeListener(wasmEnabled, () async {
       final enabled = wasmEnabled.value;
       _log.fine('preference update (wasmEnabled = $enabled)');
@@ -247,6 +269,23 @@
     toggleWasmEnabled(shouldEnableWasm);
   }
 
+  Future<void> _maybeShowWasmWelcomeMessage() async {
+    // If we have already shown the welcome message, don't show it again.
+    final welcomeAlreadyShown = await storage.getValue(_welcomeShownStorageId);
+    if (welcomeAlreadyShown == 'true') return;
+
+    // Show the welcome message if the WASM experiment is enabled but the user
+    // is not using the WASM build.
+    final connectedApp = serviceConnection.serviceManager.connectedApp;
+    if (connectedApp != null &&
+        FeatureFlags.wasmByDefault.isEnabled(connectedApp) &&
+        !kIsWasm) {
+      // Mark the welcome message as shown.
+      await storage.setValue(_welcomeShownStorageId, 'true');
+      pushWasmWelcomeMessage();
+    }
+  }
+
   Future<void> _initVerboseLogging() async {
     final verboseLoggingEnabledValue = await boolValueFromStorage(
       _GeneralPreferences.verboseLogging.name,
diff --git a/packages/devtools_app_shared/lib/src/utils/url/_url_stub.dart b/packages/devtools_app_shared/lib/src/utils/url/_url_stub.dart
index 8f9649e..0990d64 100644
--- a/packages/devtools_app_shared/lib/src/utils/url/_url_stub.dart
+++ b/packages/devtools_app_shared/lib/src/utils/url/_url_stub.dart
@@ -17,6 +17,11 @@
 // Unused parameter lint doesn't make sense for stub files.
 void webRedirect(String url) {}
 
+/// Performs a web reload using window.location.reload().
+///
+/// No-op for non-web platforms.
+void webReload() {}
+
 /// Updates the query parameter with [key] to the new [value], and optionally
 /// reloads the page when [reload] is true.
 ///
diff --git a/packages/devtools_app_shared/lib/src/utils/url/_url_web.dart b/packages/devtools_app_shared/lib/src/utils/url/_url_web.dart
index 7ca399d..67e84ee 100644
--- a/packages/devtools_app_shared/lib/src/utils/url/_url_web.dart
+++ b/packages/devtools_app_shared/lib/src/utils/url/_url_web.dart
@@ -16,6 +16,10 @@
   window.location.replace(url);
 }
 
+void webReload() {
+  window.location.reload();
+}
+
 void updateQueryParameter(String key, String? value, {bool reload = false}) {
   final newQueryParams = Map.of(loadQueryParams());
   if (value == null) {