Repository: wiredashio/wiredash-sdk

Issue:
(Beta 5) "No MaterialLocalizations found" Error when using custom wiredash localizations delegate
It appears that when I activate Wiredash using a localizationDelegate in the WiredashOptionsData to override the UI strings, the localizations are stripped from the rest of the app.

The Wiredash translations are working fine.

I get the following error attempting to open Wiredash. The app runs fine until Wiredash is attempted to be opened. (The AppBar uses `AppLocalizations.of(context)!.translationString`). Additional similar errors for each line using AppLocalizations for the given context.

```
======== Exception caught by widgets library =======================================================
The following assertion was thrown building AppBar(dirty, dependencies: [_ScrollNotificationObserverScope, _LocalizationsScope-[GlobalKey#9becd]], state: _AppBarState#5aac7):
No MaterialLocalizations found.

AppBar widgets require MaterialLocalizations to be provided by a Localizations widget ancestor.
The material library uses Localizations to generate messages, labels, and abbreviations.

To introduce a MaterialLocalizations, either use a MaterialApp at the root of your application to include them automatically, or add a Localization widget with a MaterialLocalizations delegate.
```

<img width="847" alt="wiredash-error" src="https://user-images.githubusercontent.com/3958274/178590823-4c3c5f77-1451-47a4-98e0-6f2c048c19f8.png">


**main.dart**: Using MaterialApp.router with normal [AppLocalizations implementation from flutter docs](https://docs.flutter.dev/development/accessibility-and-localization/internationalization). 
```dart
          child: MaterialApp.router(
            locale: _locale,
            supportedLocales: supportedLocales,
            localizationsDelegates: AppLocalizations.localizationsDelegates,
            debugShowCheckedModeBanner: false,
            scaffoldMessengerKey: rootScaffoldMessengerKey,
            title: 'XXX',
            theme: ConnectTheme.themeData(context),
            routerDelegate: RoutemasterDelegate(
              observers: [
                ConnectRouteObserver(
                  firebaseAnalytics: firebaseAnalytics,
                  user: user,
                  ref: ref,
                ),
              ],
              routesBuilder: (context) {
                // Watch for user state changes to rebuild our RouteMap based on user login state
                RouteMap _routeMap = ref.watch(routeMapProvider);
                return _routeMap;
              },
            ),
            routeInformationParser: const RoutemasterParser(),
          ),
```

**Wrapper widget for all screens, where Wiredash is parent**
```dart
              // User exists (Wiredash must be here because we need access to a BuildContext after Localizations have been initialized.
              // in order to get translations for our labels.
              // and this widget is a wrapper for all authenticated user screens.
              return Wiredash(
                projectId: wiredashCredentials.projectId,
                secret: wiredashCredentials.secret,
                options: WiredashOptionsData(
                  // Locale in wiredash does not support defining a country code as of 1.0.0-beta5, so derive from just the language code
                  locale: Locale.fromSubtags(languageCode: _locale.languageCode),
                  localizationDelegate: const WiredashCustomLocalizationsDelegate(),
                ),
                feedbackOptions: WiredashFeedbackOptions(
                  askForUserEmail: true,
                  labels: getWiredashLabels(context),
                ),
                child: screen,
              );
```

**Wiredash activation (via drawer)**
```dart
                    // Set Wiredash Data
                    String? userEmail = _user?.email ?? "email not available";
                    Wiredash.of(context).setUserProperties(userEmail: userEmail);
                    Wiredash.of(context).setBuildProperties(buildNumber: _buildNumber, buildVersion: _version);

                    // Launch Wiredash Interface
                    Wiredash.of(context).show(inheritMaterialTheme: true);
```

**Wiredash custom localizations delegate**
```dart
import 'package:connect/models/locale/locale_providers.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:wiredash/wiredash.dart';

/// Wiredash Custom Localizations Delegate
/// Use this to provide your own texts that match the style of your app.
/// Part of WiredashOptionsData parameters.
/// See Example: https://github.com/wiredashio/wiredash-sdk/blob/stable/example/lib/custom_demo_translations.dart
class WiredashCustomLocalizationsDelegate extends LocalizationsDelegate<WiredashLocalizations> {
  const WiredashCustomLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) {
    // Check if the provided locale language code exists in any of our supportedLocale language codes
    return supportedLocales.any((supportedLocale) => supportedLocale.languageCode == locale.languageCode);
  }

  @override
  Future<WiredashLocalizations> load(Locale locale) {
    switch (locale.languageCode) {
      case "en":
        {
          return SynchronousFuture(_CustomTranslationsEn());
        }

      case "de":
        {
          return SynchronousFuture(_CustomTranslationsDe());
        }

      default:
        return SynchronousFuture(_CustomTranslationsEn());
    }
  }

  @override
  bool shouldReload(covariant LocalizationsDelegate<WiredashLocalizations> old) => false;
}

/// Override English Wiredash Strings
class _CustomTranslationsEn extends WiredashLocalizationsEn {
  @override
  String get feedbackStep1MessageDescription =>
      "Add description for your request functions and problems you encountered in xxx";

  @override
  String get feedbackStep1MessageHint =>
      "I want to order ink from xxx... / There's an unknown error when I check printer monitoring...";
}

/// Override German Wiredash Strings
class _CustomTranslationsDe extends WiredashLocalizationsEn {
  @override
  String get feedbackStep1MessageDescription =>
      "Fügen Sie eine Beschreibung der von Ihnen angefragten Funktionen und der Probleme hinzu, auf die Sie in xxx gestoßen sind";

  @override
  String get feedbackStep1MessageHint =>
      "Ich möchte Tinte bei xxx bestellen... / Es gibt einen unbekannten Fehler, wenn ich die Druckerüberwachung prüfe...";
}
```


Discussion: Thanks for such detailed reporting 💙

The issue is caused because `Wiredash` sits below `MaterialApp` that internally injects the `Localizations` widget into the tree. Wiredash indeed overrides the `Localizations` widget, dropping existing delegates.

The correct order is to have `Wiredash` sit above the `MaterialApp`. From the code you provided that should be doable because `_locale` is known when you construct `MaterialApp`.

```dart
return Wiredash(
  projectId: wiredashCredentials.projectId,
  secret: wiredashCredentials.secret,
  options: WiredashOptionsData(
    // Locale in wiredash does not support defining a country code as of 1.0.0-beta5, so derive from just the language code
    locale: Locale.fromSubtags(languageCode: _locale.languageCode),
    localizationDelegate: const WiredashCustomLocalizationsDelegate(),
  ),
  feedbackOptions: WiredashFeedbackOptions(
    askForUserEmail: true,
    labels: getWiredashLabels(context),
  ),
  child: MaterialApp.router(
    locale: _locale,
    supportedLocales: supportedLocales,
    localizationsDelegates: AppLocalizations.localizationsDelegates,
    debugShowCheckedModeBanner: false,
    scaffoldMessengerKey: rootScaffoldMessengerKey,
    title: 'XXX',
    theme: ConnectTheme.themeData(context),
    routerDelegate: RoutemasterDelegate(
      observers: [
        ConnectRouteObserver(
          firebaseAnalytics: firebaseAnalytics,
          user: user,
          ref: ref,
        ),
      ],
      routesBuilder: (context) {
        // Watch for user state changes to rebuild our RouteMap based on user login state
        RouteMap _routeMap = ref.watch(routeMapProvider);
        return _routeMap;
      },
    ),
    routeInformationParser: const RoutemasterParser(),
  );
```
---

Nevertheless, I look to find a way to continue providing existing localizationDelegates. Should be doable.
The issue I had with putting Wiredash above `MaterialApp.router` is that in order to get the translations for our labels (under feedbackOptions), we need access to a BuildContext that has AppLocalizations instantiated, which only happens below MaterialApp.

Example of our translated label list:

```dart
/// Wiredash Labels List
List<Label> getWiredashLabels(BuildContext context) {
  return <Label>[
    Label(id: "label-vku...", title: AppLocalizations.of(context)!.labelsPraise),
    Label(id: "label-oav...", title: AppLocalizations.of(context)!.labelsBug),
    Label(id: "label-da6...", title: AppLocalizations.of(context)!.labelsImprovement),
    Label(id: "label-acn...", title: AppLocalizations.of(context)!.labelsNewIdea),
  ];
}
```

Throws error:
```
The following _CastError was thrown building Consumer(dirty, dependencies: [UncontrolledProviderScope], state: _ConsumerState#2a30a):
Null check operator used on a null value

The relevant error-causing widget was: 
  Consumer Consumer:file:///Users/matt/repos/roland/rdgconnect/lib/main.dart:150:12
When the exception was thrown, this was the stack: 
#0      getWiredashLabels (package:connect/models/wiredash/constants_wiredash.dart:18:70)
```

Can you suggest a better option for getting translated labels from our .arb files if Wiredash needs to be above MaterialApp?

```dart
  @override
  String get feedbackStep1MessageHint =>
      "Ich möchte Tinte bei xxx bestellen... / Es gibt einen unbekannten Fehler, wenn ich die Druckerüberwachung prüfe...";
```

For the overrides of the Wiredash Localizations, we had to use strings in the getters where we would really prefer to use the .arb translation files we are already using for the rest of the app. Is this possible? We don't seem to have a build context here to get the translations either. And the alternative method of using a delegate loader like this requires an async function to load, which is not a valid override.

```dart
  @override
  String get feedbackStep1MessageDescription async {
    final l10n = await AppLocalizations.delegate.load(locale);
    return l10n.labelsNewIdea
  }
```

Our locale is dynamic using Riverpod state, so when the user changes locale, the state of locale updates rebuilding the widgets. So when the locale is changed, we also do not have an opportunity to introduce an async function during that rebuild.

```dart
@override
  Widget build(BuildContext context) {
    return Consumer(builder: (context, ref, child) {
      // Watch for locale changes
      Locale _locale = ref.watch(localeProvider);
...
    return Wiredash(
      ...
      child: MaterialApp.router(
        ...
```
Got it. Yes, the label localization needs to be revisited and smoothed out. I'll write an example for this.

Meanwhile, please try the fix in #224 I just merged. It allows you to place `Wiredash` below `MaterialApp`

```yaml
dependencies:
  wiredash:
    git:
      url: https://github.com/wiredashio/wiredash-sdk.git
      revision: a4c3a913eca1ce8c63f10bdcd9ebf207fbfa2f54
```

---

The best position for Wiredash is before any UI and after all non-UI things that need to be provided. i.e.

```
--- Non UI ---
FeatureFlags
Localizations
NetworkingProvider
...
Wiredash
--- UI ---
MaterialApp
Scaffold
...
```
Thank you so much I will try that out.

I edited the response, so not sure if you saw the addition at the end about using our existing .arb file localizations on the String overrides in the custom wiredash delegate? Not sure if or how that may be possible. 
> Meanwhile, please try the fix in #224 I just merged. It allows you to place `Wiredash` below `MaterialApp`

This works great. Thanks again. 
> Can you suggest a better option for getting translated labels from our .arb files if Wiredash needs to be above MaterialApp?

You can inject the loaded `AppLocalizations` into the `_DeOverrides`. It can work like this:

```dart
class CustomWiredashTranslationsDelegate
    extends LocalizationsDelegate<WiredashLocalizations> {
  const CustomWiredashTranslationsDelegate();

  @override
  bool isSupported(Locale locale) {
    return ['de'].contains(locale.languageCode);
  }

  @override
  Future<WiredashLocalizations> load(Locale locale) async {
    if (locale.languageCode == 'de') {
      final localization = await AppLocalizations.delegate.load(locale);
      return _DeOverrides(localization);
    }
    throw "Unsupported locale $locale";
  }

  @override
  bool shouldReload(CustomWiredashTranslationsDelegate old) => false;
}
```

```dart
class _DeOverrides extends WiredashLocalizationsDe {
  final AppLocalizations localizations;

  _DeOverrides(this.localizations);

  @override
  String get feedbackStep1MessageTitle => localizations.helloWorld;
}
```

(based on the sample in https://github.com/wiredashio/wiredash-sdk/blob/stable/examples/localization/lib/main.dart)

Fix patch:
diff --git a/examples/localization/analysis_options.yaml b/examples/localization/analysis_options.yaml
index 20889b04..5a62dccf 100644
--- a/examples/localization/analysis_options.yaml
+++ b/examples/localization/analysis_options.yaml
@@ -3,10 +3,11 @@ include: package:lint/analysis_options.yaml
 analyzer:
   exclude:
     - 'lib/src/generated_plugin_registrant.dart'
+    - 'lib/l10n/*.dart'
 
 linter:
   rules:
     depend_on_referenced_packages: false
     prefer_const_constructors: false
     prefer_const_literals_to_create_immutables: false
-    prefer_const_constructors_in_immutables: false
\ No newline at end of file
+    prefer_const_constructors_in_immutables: false
diff --git a/examples/localization/lib/l10n/app_de.arb b/examples/localization/lib/l10n/app_de.arb
index 382dfdf8..67ba6051 100644
--- a/examples/localization/lib/l10n/app_de.arb
+++ b/examples/localization/lib/l10n/app_de.arb
@@ -1,4 +1,6 @@
 {
   "wiredashFeedbackStep1MessageTitle": "Benutzerdefinierter Titel",
-  "wiredashFeedbackStep1MessageDescription": "Benutzerdefinierte Bescheibung"
+  "wiredashFeedbackStep1MessageDescription": "Benutzerdefinierte Bescheibung",
+  "labelA": "Kennzeichen A",
+  "labelB": "Kennzeichen B"
 }
\ No newline at end of file
diff --git a/examples/localization/lib/l10n/app_en.arb b/examples/localization/lib/l10n/app_en.arb
index d05660df..d00bbcec 100644
--- a/examples/localization/lib/l10n/app_en.arb
+++ b/examples/localization/lib/l10n/app_en.arb
@@ -1,4 +1,6 @@
 {
   "wiredashFeedbackStep1MessageTitle": "Custom title",
-  "wiredashFeedbackStep1MessageDescription": "Custom description"
+  "wiredashFeedbackStep1MessageDescription": "Custom description",
+  "labelA": "Label A",
+  "labelB": "Label B"
 }
\ No newline at end of file
diff --git a/examples/localization/lib/l10n/app_localizations.dart b/examples/localization/lib/l10n/app_localizations.dart
index 468f846e..09acc585 100644
--- a/examples/localization/lib/l10n/app_localizations.dart
+++ b/examples/localization/lib/l10n/app_localizations.dart
@@ -1,4 +1,3 @@
-
 import 'dart:async';
 
 import 'package:flutter/foundation.dart';
@@ -61,15 +60,17 @@ import 'app_localizations_en.dart';
 /// be consistent with the languages listed in the AppLocalizations.supportedLocales
 /// property.
 abstract class AppLocalizations {
-  AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
+  AppLocalizations(String locale)
+      : localeName = intl.Intl.canonicalizedLocale(locale.toString());
 
   final String localeName;
 
-  static AppLocalizations of(BuildContext context) {
-    return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
+  static AppLocalizations? of(BuildContext context) {
+    return Localizations.of<AppLocalizations>(context, AppLocalizations);
   }
 
-  static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
+  static const LocalizationsDelegate<AppLocalizations> delegate =
+      _AppLocalizationsDelegate();
 
   /// A list of this localizations delegate along with the default localizations
   /// delegates.
@@ -81,7 +82,8 @@ abstract class AppLocalizations {
   /// Additional delegates can be added by appending to this list in
   /// MaterialApp. This list does not have to be used at all if a custom list
   /// of delegates is preferred or required.
-  static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
+  static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
+      <LocalizationsDelegate<dynamic>>[
     delegate,
     GlobalMaterialLocalizations.delegate,
     GlobalCupertinoLocalizations.delegate,
@@ -105,9 +107,22 @@ abstract class AppLocalizations {
   /// In en, this message translates to:
   /// **'Custom description'**
   String get wiredashFeedbackStep1MessageDescription;
+
+  /// No description provided for @labelA.
+  ///
+  /// In en, this message translates to:
+  /// **'Label A'**
+  String get labelA;
+
+  /// No description provided for @labelB.
+  ///
+  /// In en, this message translates to:
+  /// **'Label B'**
+  String get labelB;
 }
 
-class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
+class _AppLocalizationsDelegate
+    extends LocalizationsDelegate<AppLocalizations> {
   const _AppLocalizationsDelegate();
 
   @override
@@ -116,25 +131,25 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations>
   }
 
   @override
-  bool isSupported(Locale locale) => <String>['de', 'en'].contains(locale.languageCode);
+  bool isSupported(Locale locale) =>
+      <String>['de', 'en'].contains(locale.languageCode);
 
   @override
   bool shouldReload(_AppLocalizationsDelegate old) => false;
 }
 
 AppLocalizations lookupAppLocalizations(Locale locale) {
-
-
   // Lookup logic when only language code is specified.
   switch (locale.languageCode) {
-    case 'de': return AppLocalizationsDe();
-    case 'en': return AppLocalizationsEn();
+    case 'de':
+      return AppLocalizationsDe();
+    case 'en':
+      return AppLocalizationsEn();
   }
 
   throw FlutterError(
-    'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
-    'an issue with the localizations generation tool. Please file an issue '
-    'on GitHub with a reproducible sample app and the gen-l10n configuration '
-    'that was used.'
-  );
+      'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
+      'an issue with the localizations generation tool. Please file an issue '
+      'on GitHub with a reproducible sample app and the gen-l10n configuration '
+      'that was used.');
 }
diff --git a/examples/localization/lib/l10n/app_localizations_de.dart b/examples/localization/lib/l10n/app_localizations_de.dart
index edea514c..1d3b0336 100644
--- a/examples/localization/lib/l10n/app_localizations_de.dart
+++ b/examples/localization/lib/l10n/app_localizations_de.dart
@@ -1,6 +1,3 @@
-
-
-
 import 'app_localizations.dart';
 
 /// The translations for German (`de`).
@@ -11,5 +8,12 @@ class AppLocalizationsDe extends AppLocalizations {
   String get wiredashFeedbackStep1MessageTitle => 'Benutzerdefinierter Titel';
 
   @override
-  String get wiredashFeedbackStep1MessageDescription => 'Benutzerdefinierte Bescheibung';
+  String get wiredashFeedbackStep1MessageDescription =>
+      'Benutzerdefinierte Bescheibung';
+
+  @override
+  String get labelA => 'Kennzeichen A';
+
+  @override
+  String get labelB => 'Kennzeichen B';
 }
diff --git a/examples/localization/lib/l10n/app_localizations_en.dart b/examples/localization/lib/l10n/app_localizations_en.dart
index 2f6a58d5..12197414 100644
--- a/examples/localization/lib/l10n/app_localizations_en.dart
+++ b/examples/localization/lib/l10n/app_localizations_en.dart
@@ -1,6 +1,3 @@
-
-
-
 import 'app_localizations.dart';
 
 /// The translations for English (`en`).
@@ -12,4 +9,10 @@ class AppLocalizationsEn extends AppLocalizations {
 
   @override
   String get wiredashFeedbackStep1MessageDescription => 'Custom description';
+
+  @override
+  String get labelA => 'Label A';
+
+  @override
+  String get labelB => 'Label B';
 }
diff --git a/examples/localization/lib/main.dart b/examples/localization/lib/main.dart
index 4465d8cb..e47ba5c6 100644
--- a/examples/localization/lib/main.dart
+++ b/examples/localization/lib/main.dart
@@ -2,7 +2,6 @@ import 'dart:ui';
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_localizations/flutter_localizations.dart';
 import 'package:wiredash/wiredash.dart';
 import 'package:wiredash_localization/l10n/app_localizations.dart';
 
@@ -44,13 +43,10 @@ class _LocalizationExampleState extends State<LocalizationExample> {
           Locale('ko'),
         ],
         localizationsDelegates: [
-          GlobalMaterialLocalizations.delegate,
-          GlobalCupertinoLocalizations.delegate,
-          GlobalWidgetsLocalizations.delegate,
+          ...AppLocalizations.localizationsDelegates,
         ],
         theme: ThemeData(
-          primarySwatch: Colors.blue,
-          brightness: Brightness.light,
+          primarySwatch: Colors.green,
         ),
         home: Builder(
           builder: _buildPage,
@@ -66,7 +62,25 @@ class _LocalizationExampleState extends State<LocalizationExample> {
       ),
       floatingActionButton: FloatingActionButton(
         onPressed: () {
-          Wiredash.of(context).show();
+          Wiredash.of(context).show(
+            inheritMaterialTheme: true,
+            feedbackOptions: WiredashFeedbackOptions(
+              email: EmailPrompt.optional,
+              screenshot: ScreenshotPrompt.optional,
+              labels: [
+                Label(
+                  id: 'label-a',
+                  title: AppLocalizations.of(context)?.labelA ??
+                      "Fallback for Label A",
+                ),
+                Label(
+                  id: 'label-b',
+                  title: AppLocalizations.of(context)?.labelB ??
+                      "Fallback for Label B",
+                ),
+              ],
+            ),
+          );
         },
         child: Icon(Icons.chat),
       ),
@@ -199,7 +213,7 @@ class _EnOverrides extends WiredashLocalizationsEn {
 
 /// Injects copy text from [AppLocalizations] into Wiredash
 ///
-/// Run `flutter gen-l10n --no-synthetic-package` to generate [AppLocalizations]
+/// Run `flutter gen-l10n --no-synthetic-package --nullable-getter` to generate [AppLocalizations]
 class _DeOverrides extends WiredashLocalizationsDe {
   final AppLocalizations localizations;
 
diff --git a/examples/theming/lib/main.dart b/examples/theming/lib/main.dart
index bd5e873a..6ce01289 100644
--- a/examples/theming/lib/main.dart
+++ b/examples/theming/lib/main.dart
@@ -33,7 +33,7 @@ class _WiredashExampleAppState extends State<WiredashExampleApp> {
       secret: "API Key from console.wiredash.io",
       feedbackOptions: WiredashFeedbackOptions(
         /// Uncomment below to ask users for their email
-        askForUserEmail: true,
+        email: EmailPrompt.optional,
 
         /// Uncomment below to disable the screenshot step
         // screenshotStep: false,
diff --git a/lib/assets/l10n/wiredash_localizations.g.dart b/lib/assets/l10n/wiredash_localizations.g.dart
index 2e61eb3f..e99a2b2e 100644
--- a/lib/assets/l10n/wiredash_localizations.g.dart
+++ b/lib/assets/l10n/wiredash_localizations.g.dart
@@ -8,14 +8,14 @@ import 'package:intl/intl.dart' as intl;
 import 'wiredash_localizations_de.g.dart';
 import 'wiredash_localizations_en.g.dart';
 
-/// Callers can lookup localized strings with an instance of WiredashLocalizations
-/// returned by `WiredashLocalizations.of(context)`.
+/// Callers can lookup localized strings with an instance of WiredashLocalizations returned
+/// by `WiredashLocalizations.of(context)`.
 ///
 /// Applications need to include `WiredashLocalizations.delegate()` in their app's
-/// `localizationDelegates` list, and the locales they support in the app's
-/// `supportedLocales` list. For example:
+/// localizationDelegates list, and the locales they support in the app's
+/// supportedLocales list. For example:
 ///
-/// ```dart
+/// ```
 /// import 'l10n/wiredash_localizations.g.dart';
 ///
 /// return MaterialApp(
@@ -30,14 +30,14 @@ import 'wiredash_localizations_en.g.dart';
 /// Please make sure to update your pubspec.yaml to include the following
 /// packages:
 ///
-/// ```yaml
+/// ```
 /// dependencies:
 ///   # Internationalization support.
 ///   flutter_localizations:
 ///     sdk: flutter
 ///   intl: any # Use the pinned version from flutter_localizations
 ///
-///   # Rest of dependencies
+///   # rest of dependencies
 /// ```
 ///
 /// ## iOS Applications
diff --git a/lib/src/core/options/feedback_options.dart b/lib/src/core/options/feedback_options.dart
index a99cb0c1..c21af86e 100644
--- a/lib/src/core/options/feedback_options.dart
+++ b/lib/src/core/options/feedback_options.dart
@@ -18,18 +18,21 @@ class WiredashFeedbackOptions {
   /// labels yourself, you know best if and how they should be translated.
   final List<Label>? labels;
 
-  /// When `true` a step asking the user for their email address is shown
+  /// Define if and how users get asked for their email address.
   ///
-  /// Defaults to false, does not show the email address step.
-  final bool? askForUserEmail;
+  /// Defaults to [EmailPrompt.optional]
+  final EmailPrompt? email;
 
-  /// Whether to display the screenshot and drawing step or not.
+  /// Define whether users can attach screenshots
   ///
-  /// Defaults to true, allows the user to add screenshots
+  /// Defaults to [ScreenshotPrompt.optional]
   ///
-  /// The Flutter Web beta does not currently support screenshots. Therefore,
-  /// the screenshot and drawing step is never shown and this option is ignored.
-  final bool? screenshotStep;
+  /// Screenshots are not supported on Flutter Web when using the html renderer.
+  /// The screenshot step will therefore not be shown on Flutter Web when using
+  /// the html renderer.
+  /// https://github.com/flutter/flutter/issues/59072
+  /// https://github.com/flutter/flutter/issues/49857
+  final ScreenshotPrompt? screenshot;
 
   /// Enrich the user feedback with custom metadata
   ///
@@ -51,21 +54,54 @@ class WiredashFeedbackOptions {
 
   const WiredashFeedbackOptions({
     this.labels,
-    this.askForUserEmail,
+    @Deprecated('Use `email` instead') bool? askForUserEmail,
+    EmailPrompt? email,
     this.collectMetaData,
-    this.screenshotStep,
-  });
+    @Deprecated('Use `screenshot` instead') bool? screenshotStep,
+    ScreenshotPrompt? screenshot,
+  })  : screenshot = screenshot ??
+            (screenshotStep == true
+                ? ScreenshotPrompt.optional
+                : screenshotStep == false
+                    ? ScreenshotPrompt.hidden
+                    : null),
+        email = email ??
+            (askForUserEmail == true
+                ? EmailPrompt.optional
+                : askForUserEmail == false
+                    ? EmailPrompt.hidden
+                    : null);
 
   @override
   String toString() {
     return 'WiredashFeedbackOptions{'
         'labels: $labels, '
-        'askForUserEmail: $askForUserEmail'
-        'screenshotStep: $screenshotStep'
+        'email: $email'
+        'screenshot: $screenshot'
         '}';
   }
 }
 
+enum EmailPrompt {
+  /// Email step is not shown
+  hidden,
+
+  /// User is optionally asked for their email address
+  optional,
+
+  /// The email address is mandatory
+  // TODO implement
+  //mandatory,
+}
+
+enum ScreenshotPrompt {
+  /// Screenshot step is not shown
+  hidden,
+
+  /// User is asked to optionally attach screenshots
+  optional,
+}
+
 /// Mutable version of [WiredashMetaData] that will be sent along the user
 /// feedback to the Wiredash console
 ///
diff --git a/lib/src/core/wiredash_controller.dart b/lib/src/core/wiredash_controller.dart
index 66df2c26..92719445 100644
--- a/lib/src/core/wiredash_controller.dart
+++ b/lib/src/core/wiredash_controller.dart
@@ -158,9 +158,11 @@ class WiredashController {
   void show({
     bool? inheritMaterialTheme,
     bool? inheritCupertinoTheme,
+    WiredashFeedbackOptions? feedbackOptions,
   }) {
     _captureAppTheme(inheritMaterialTheme, inheritCupertinoTheme);
     _captureAppLocale();
+    _model.feedbackOptionsOverride = feedbackOptions;
     _model.show(flow: WiredashFlow.feedback);
   }
 
@@ -188,7 +190,7 @@ class WiredashController {
     if (context == null) return;
 
     final locale = Localizations.maybeLocaleOf(context);
-    _model.appLocale = locale;
+    _model.appLocaleFromContext = locale;
   }
 
   /// Search the user context for the app theme
diff --git a/lib/src/core/wiredash_model.dart b/lib/src/core/wiredash_model.dart
index 243f9a3f..9f8b5984 100644
--- a/lib/src/core/wiredash_model.dart
+++ b/lib/src/core/wiredash_model.dart
@@ -53,15 +53,29 @@ class WiredashModel with ChangeNotifier {
   }
 
   /// The locale of the application where `Wiredash.of(context)` got called
-  Locale? _appLocale;
+  Locale? _appLocaleFromContext;
 
-  Locale? get appLocale => _appLocale;
+  Locale? get appLocaleFromContext => _appLocaleFromContext;
 
-  set appLocale(Locale? appLocale) {
-    _appLocale = appLocale;
+  set appLocaleFromContext(Locale? appLocale) {
+    _appLocaleFromContext = appLocale;
     notifyListeners();
   }
 
+  /// The feedback options passed into `Wiredash.of(context).show()` call
+  WiredashFeedbackOptions? _feedbackOptionsOverride;
+
+  WiredashFeedbackOptions? get feedbackOptionsOverride =>
+      _feedbackOptionsOverride;
+
+  set feedbackOptionsOverride(WiredashFeedbackOptions? feedbackOptions) {
+    _feedbackOptionsOverride = feedbackOptions;
+    notifyListeners();
+  }
+
+  WiredashFeedbackOptions? get feedbackOptions =>
+      _feedbackOptionsOverride ?? services.wiredashWidget.feedbackOptions;
+
   /// Deletes pending feedbacks
   ///
   /// Usually only relevant for debug builds
@@ -99,6 +113,12 @@ class WiredashModel with ChangeNotifier {
   }) async {
     await services.backdropController.animateToClosed();
     isWiredashActive = false;
+
+    // reset options from show() call
+    themeFromContext = null;
+    appLocaleFromContext = null;
+    feedbackOptionsOverride = null;
+
     if (discardFeedback) {
       services.discardFeedback();
     }
diff --git a/lib/src/core/wiredash_widget.dart b/lib/src/core/wiredash_widget.dart
index 3be169aa..bd01f3aa 100644
--- a/lib/src/core/wiredash_widget.dart
+++ b/lib/src/core/wiredash_widget.dart
@@ -348,7 +348,7 @@ class WiredashState extends State<Wiredash> {
       // Use what users set in WiredashOptions has the highest priority
       widget.options?.locale,
       // Use what users see in the app
-      _services.wiredashModel.appLocale
+      _services.wiredashModel.appLocaleFromContext
     ].whereNotNull();
 
     for (final locale in localesInOrder) {
diff --git a/lib/src/feedback/data/label.dart b/lib/src/feedback/data/label.dart
index 7e735f71..4ef69d42 100644
--- a/lib/src/feedback/data/label.dart
+++ b/lib/src/feedback/data/label.dart
@@ -1,16 +1,32 @@
-import 'package:flutter/foundation.dart';
+import 'package:wiredash/src/feedback/_feedback.dart';
 
-@immutable
+/// A label/tag can be assigned to a [PersistedFeedbackItem].
+///
+/// Users can select multiple labels for each feedback.
+///
+/// Hidden labels are not shown in the UI. They are always sent to the console
 class Label {
   const Label({
     required this.id,
     required this.title,
-    this.description,
+    this.hidden,
   });
 
+  /// The unique identifier of the label, generated by the console
+  ///
+  /// Grab the label id from the console https://console.wiredash.io/ at
+  /// Settings -> Labels
   final String id;
+
+  /// The title of the label, displayed to the user. You might want to use a
+  /// localized string here.
   final String title;
-  final String? description;
+
+  /// A hidden label is not visible to the user. It will be sent directly to the
+  /// console
+  ///
+  /// Defaults to `false`
+  final bool? hidden;
 
   @override
   bool operator ==(Object other) =>
@@ -19,29 +35,17 @@ class Label {
           runtimeType == other.runtimeType &&
           id == other.id &&
           title == other.title &&
-          description == other.description);
+          hidden == other.hidden);
 
   @override
-  int get hashCode => id.hashCode ^ title.hashCode ^ description.hashCode;
+  int get hashCode => id.hashCode ^ title.hashCode ^ hidden.hashCode;
 
   @override
   String toString() {
     return 'Label{'
         'id: $id, '
         'title: $title, '
-        'description: $description'
+        'hidden: $hidden'
         '}';
   }
-
-  Label copyWith({
-    String? id,
-    String? title,
-    String? description,
-  }) {
-    return Label(
-      id: id ?? this.id,
-      title: title ?? this.title,
-      description: description ?? this.description,
-    );
-  }
 }
diff --git a/lib/src/feedback/feedback_model.dart b/lib/src/feedback/feedback_model.dart
index fa11ecc0..52423f1a 100644
--- a/lib/src/feedback/feedback_model.dart
+++ b/lib/src/feedback/feedback_model.dart
@@ -50,7 +50,7 @@ class FeedbackModel extends ChangeNotifier2 {
   List<Label> _selectedLabels = [];
 
   List<Label> get labels =>
-      _services.wiredashWidget.feedbackOptions?.labels ?? [];
+      _services.wiredashModel.feedbackOptions?.labels ?? [];
 
   List<PersistedAttachment> get attachments =>
       _attachments.toList(growable: false);
@@ -88,10 +88,10 @@ class FeedbackModel extends ChangeNotifier2 {
     // screenshot
     var steps = 2;
 
-    if (_services.wiredashWidget.feedbackOptions?.askForUserEmail == true) {
+    if (_services.wiredashModel.feedbackOptions?.email != EmailPrompt.hidden) {
       steps++;
     }
-    if (_services.wiredashWidget.feedbackOptions?.labels?.isNotEmpty == true) {
+    if (_services.wiredashModel.feedbackOptions?.labels?.isNotEmpty == true) {
       steps++;
     }
 
@@ -121,15 +121,20 @@ class FeedbackModel extends ChangeNotifier2 {
     // message is always there
     stack.add(FeedbackFlowStatus.message);
 
-    if (labels.isNotEmpty) stack.add(FeedbackFlowStatus.labels);
+    if (labels.where((it) => it.hidden != true).isNotEmpty) {
+      stack.add(FeedbackFlowStatus.labels);
+    }
     final renderer = getRenderer();
-    if (_services.wiredashWidget.feedbackOptions?.screenshotStep != false &&
-        renderer != Renderer.html) {
+    if (_services.wiredashModel.feedbackOptions?.screenshot == null ||
+        _services.wiredashModel.feedbackOptions?.screenshot ==
+                ScreenshotPrompt.optional &&
+            renderer != Renderer.html) {
       // Don't show the screenshot option with html renderer, because it
       // doesn't support rendering to canvas
       stack.add(FeedbackFlowStatus.screenshotsOverview);
     }
-    if (_services.wiredashWidget.feedbackOptions?.askForUserEmail == true) {
+    if (_services.wiredashModel.feedbackOptions?.email ==
+        EmailPrompt.optional) {
       stack.add(FeedbackFlowStatus.email);
     }
     stack.add(FeedbackFlowStatus.submit);
@@ -207,6 +212,9 @@ class FeedbackModel extends ChangeNotifier2 {
   void goToPreviousStep() {
     final index = currentStepIndex;
     if (index == null) {
+      if (!steps.contains(feedbackFlowStatus)) {
+        debugPrint('Warning: $feedbackFlowStatus is not in steps');
+      }
       throw StateError('Unknown step index');
     }
     final prevStepIndex = index - 1;
@@ -280,8 +288,10 @@ class FeedbackModel extends ChangeNotifier2 {
     _deviceInfo = _services.deviceInfoGenerator.generate();
     final metaData = _services.wiredashModel.metaData;
     // Allow devs to collect additional information
-    await _services.wiredashWidget.feedbackOptions?.collectMetaData
-        ?.call(metaData);
+    final collector =
+        _services.wiredashWidget.feedbackOptions?.collectMetaData ??
+            _services.wiredashModel.feedbackOptionsOverride?.collectMetaData;
+    await collector?.call(metaData);
     _services.wiredashModel.metaData = metaData;
     _collectedMetadataForScreenshot = true;
     notifyListeners();
@@ -368,9 +378,10 @@ class FeedbackModel extends ChangeNotifier2 {
     CustomizableWiredashMetaData metaData = _services.wiredashModel.metaData;
     if (!_collectedMetadataForScreenshot) {
       // Allow devs to collect additional information
-      final updated = await _services
-          .wiredashWidget.feedbackOptions?.collectMetaData
-          ?.call(metaData);
+      final collector =
+          _services.wiredashWidget.feedbackOptions?.collectMetaData ??
+              _services.wiredashModel.feedbackOptionsOverride?.collectMetaData;
+      final updated = await collector?.call(metaData);
       if (updated != null) {
         metaData = updated;
         _services.wiredashModel.metaData = metaData;
@@ -381,7 +392,8 @@ class FeedbackModel extends ChangeNotifier2 {
     return PersistedFeedbackItem(
       appInfo: AppInfo(
         appLocale:
-            _services.wiredashModel.appLocale?.toLanguageTag() ?? 'unknown',
+            _services.wiredashModel.appLocaleFromContext?.toLanguageTag() ??
+                'unknown',
       ),
       attachments: _attachments,
       buildInfo: buildInfo.copyWith(
@@ -393,7 +405,8 @@ class FeedbackModel extends ChangeNotifier2 {
       deviceId: deviceId,
       deviceInfo: _deviceInfo,
       email: () {
-        if (_services.wiredashWidget.feedbackOptions?.askForUserEmail == true &&
+        if (_services.wiredashModel.feedbackOptions?.email ==
+                EmailPrompt.optional &&
             userEmail == null) {
           // user has explicitly deleted their email address
           return null;
@@ -401,7 +414,9 @@ class FeedbackModel extends ChangeNotifier2 {
         return userEmail;
       }(),
       message: _feedbackMessage!,
-      labels: _selectedLabels.map((it) => it.id).toList(),
+      labels: [..._selectedLabels, ...labels.where((it) => it.hidden == true)]
+          .map((it) => it.id)
+          .toList(),
       userId: metaData.userId,
     );
   }
diff --git a/lib/src/feedback/steps/step_2_labels.dart b/lib/src/feedback/steps/step_2_labels.dart
index f6f532ed..5c9561c6 100644
--- a/lib/src/feedback/steps/step_2_labels.dart
+++ b/lib/src/feedback/steps/step_2_labels.dart
@@ -30,7 +30,9 @@ class _Step2LabelsState extends State<Step2Labels>
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
           _LabelRecommendations(
-            labels: feedbackModel.labels,
+            labels: feedbackModel.labels
+                .where((label) => label.hidden != true)
+                .toList(),
             isLabelSelected: selectedLabels.contains,
             toggleSelection: (label) {
               setState(() {
diff --git a/lib/src/nps/nps_model.dart b/lib/src/nps/nps_model.dart
index 4a65aa97..c2885899 100644
--- a/lib/src/nps/nps_model.dart
+++ b/lib/src/nps/nps_model.dart
@@ -11,6 +11,7 @@ class NpsModel extends ChangeNotifier2 {
 
   final WiredashServices _services;
   Delay? _closeDelay;
+
   NpsScore? get score => _score;
   NpsScore? _score;
 
@@ -39,8 +40,10 @@ class NpsModel extends ChangeNotifier2 {
       final deviceInfo = _services.deviceInfoGenerator.generate();
       final metaData = _services.wiredashModel.metaData;
       // Allow devs to collect additional information
-      await _services.wiredashWidget.feedbackOptions?.collectMetaData
-          ?.call(metaData);
+      final collector =
+          _services.wiredashWidget.feedbackOptions?.collectMetaData ??
+              _services.wiredashModel.feedbackOptionsOverride?.collectMetaData;
+      await collector?.call(metaData);
 
       final body = NpsRequestBody(
         score: score!,
@@ -50,7 +53,8 @@ class NpsModel extends ChangeNotifier2 {
         deviceId: deviceId,
         userId: metaData.userId,
         userEmail: metaData.userEmail,
-        appLocale: _services.wiredashModel.appLocale?.toLanguageTag(),
+        appLocale:
+            _services.wiredashModel.appLocaleFromContext?.toLanguageTag(),
         platformLocale: deviceInfo.platformLocale,
         platformOS: deviceInfo.platformOS,
         platformUserAgent: deviceInfo.userAgent,


Test patch:
diff --git a/test/email_validation_test.dart b/test/email_validation_test.dart
index 67c6db34..28fee137 100644
--- a/test/email_validation_test.dart
+++ b/test/email_validation_test.dart
@@ -122,7 +122,7 @@ extension on WidgetTester {
     final robot = await WiredashTestRobot.launchApp(
       this,
       feedbackOptions: const WiredashFeedbackOptions(
-        askForUserEmail: true,
+        email: EmailPrompt.optional,
       ),
     );
 
diff --git a/test/feedback_flow_test.dart b/test/feedback_flow_test.dart
index 90d42ff4..88d0d4e2 100644
--- a/test/feedback_flow_test.dart
+++ b/test/feedback_flow_test.dart
@@ -1,5 +1,6 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:shared_preferences/shared_preferences.dart';
+import 'package:wiredash/src/core/widgets/backdrop/wiredash_backdrop.dart';
 import 'package:wiredash/src/core/widgets/larry_page_view.dart';
 import 'package:wiredash/src/feedback/_feedback.dart';
 import 'package:wiredash/wiredash.dart';
@@ -97,8 +98,8 @@ void main() {
         tester,
         feedbackOptions: const WiredashFeedbackOptions(
           labels: [
-            Label(id: 'lbl-1', title: 'One', description: 'First'),
-            Label(id: 'lbl-2', title: 'Two', description: 'Second'),
+            Label(id: 'lbl-1', title: 'One'),
+            Label(id: 'lbl-2', title: 'Two'),
           ],
         ),
       );
@@ -128,7 +129,7 @@ void main() {
       final robot = await WiredashTestRobot.launchApp(
         tester,
         feedbackOptions: const WiredashFeedbackOptions(
-          askForUserEmail: true,
+          email: EmailPrompt.optional,
         ),
       );
 
@@ -153,10 +154,10 @@ void main() {
       final robot = await WiredashTestRobot.launchApp(
         tester,
         feedbackOptions: const WiredashFeedbackOptions(
-          askForUserEmail: true,
+          email: EmailPrompt.optional,
           labels: [
-            Label(id: 'lbl-1', title: 'One', description: 'First'),
-            Label(id: 'lbl-2', title: 'Two', description: 'Second'),
+            Label(id: 'lbl-1', title: 'One'),
+            Label(id: 'lbl-2', title: 'Two'),
           ],
         ),
       );
@@ -196,6 +197,75 @@ void main() {
       await robot.openWiredash();
       larryPageView.childByType(Step6Submit).existsOnce();
     });
+
+    testWidgets('Dont show hidden labels but send them regardless',
+        (tester) async {
+      final robot = await WiredashTestRobot.launchApp(
+        tester,
+        feedbackOptions: const WiredashFeedbackOptions(
+          labels: [
+            Label(id: 'lbl-1', title: 'One'),
+            Label(id: 'lbl-2', title: 'Two'),
+            Label(id: 'lbl-3', title: 'Hidden', hidden: true),
+          ],
+        ),
+      );
+
+      await robot.openWiredash();
+      await robot.enterFeedbackMessage('feedback with labels');
+      await robot.goToNextStep();
+
+      // labels
+      expect(find.text('One'), findsOneWidget);
+      expect(find.text('Two'), findsOneWidget);
+      // hidden label is not shown
+      expect(find.text('Hidden'), findsNothing);
+      await robot.selectLabel('Two');
+      await robot.goToNextStep();
+
+      await robot.skipScreenshot();
+      await robot.submitFeedback();
+      await robot.waitUntilWiredashIsClosed();
+      final latestCall =
+          robot.mockServices.mockApi.sendFeedbackInvocations.latest;
+      final submittedFeedback = latestCall[0] as PersistedFeedbackItem?;
+      expect(submittedFeedback, isNotNull);
+      // 'lbl-3' is submitted but was not selected by user
+      expect(submittedFeedback!.labels, ['lbl-2', 'lbl-3']);
+      expect(submittedFeedback.message, 'feedback with labels');
+    });
+
+    testWidgets('Hidden labels only skip label step', (tester) async {
+      final robot = await WiredashTestRobot.launchApp(
+        tester,
+        feedbackOptions: const WiredashFeedbackOptions(
+          labels: [
+            Label(id: 'lbl-1', title: 'One', hidden: true),
+            Label(id: 'lbl-2', title: 'Two', hidden: true),
+          ],
+        ),
+      );
+
+      await robot.openWiredash();
+      await robot.enterFeedbackMessage('feedback with labels');
+      await robot.goToNextStep();
+
+      selectByType(Wiredash)
+          .childByType(WiredashBackdrop)
+          .childByType(LarryPageView)
+          .childByType(Step2Labels)
+          .doesNotExist();
+
+      await robot.skipScreenshot();
+      await robot.submitFeedback();
+      await robot.waitUntilWiredashIsClosed();
+      final latestCall =
+          robot.mockServices.mockApi.sendFeedbackInvocations.latest;
+      final submittedFeedback = latestCall[0] as PersistedFeedbackItem?;
+      expect(submittedFeedback, isNotNull);
+      expect(submittedFeedback!.labels, ['lbl-1', 'lbl-2']);
+      expect(submittedFeedback.message, 'feedback with labels');
+    });
   });
 }