[image_picker_web] Listens to file input cancel event. (#4453)

## Changes

This PR listens to the `cancel` event from the `input type=file` used by the web implementation of the image_picker plugin, so apps don't end up endlessly awaiting for a file that will never come **in modern browsers** (Chrome 113, Safari 16.4, or newer). _Same API as https://github.com/flutter/packages/pull/3683._

Additionally, this PR:

* Removes all code and tests mentioning `PickedFile`. (Deprecated years ago, and unused since https://github.com/flutter/packages/pull/4285) **(Breaking change)**
* Updates README to mention `XFile` which is the current return type of the package.
* Updates the dependency on `image_picker_platform_interface` to `^2.9.0`.
  * Implements all non-deprecated methods from the interface, and makes deprecated methods use the fresh ones.
  * Updates tests.

### Issues

* Fixes https://github.com/flutter/flutter/issues/92176

### Testing

* Added integration testing coverage for the 'cancel' event.
* Tested manually in Chrome with the example app running on web.
diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md
index 8230dd7..88f6159 100644
--- a/packages/image_picker/image_picker_for_web/CHANGELOG.md
+++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 3.0.0
+
+* **BREAKING CHANGE:** Removes all code and tests mentioning `PickedFile`.
+* Listens to `cancel` event on file selection. When the selection is canceled:
+  * `Future<XFile?>` methods return `null`
+  * `Future<List<XFile>>` methods return an empty list.
+
 ## 2.2.0
 
 * Adds `getMedia` method.
diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md
index 4d8db43..6583105 100644
--- a/packages/image_picker/image_picker_for_web/README.md
+++ b/packages/image_picker/image_picker_for_web/README.md
@@ -4,23 +4,12 @@
 
 ## Limitations on the web platform
 
-Since Web Browsers don't offer direct access to their users' file system,
-this plugin provides a `PickedFile` abstraction to make access uniform
-across platforms.
+### `XFile`
 
-The web version of the plugin puts network-accessible URIs as the `path`
-in the returned `PickedFile`.
+This plugin uses `XFile` objects to abstract files picked/created by the user.
 
-### URL.createObjectURL()
-
-The `PickedFile` object in web is backed by [`URL.createObjectUrl` Web API](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL),
-which is reasonably well supported across all browsers:
-
-![Data on support for the bloburls feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/bloburls.png)
-
-However, the returned `path` attribute of the `PickedFile` points to a `network` resource, and not a
-local path in your users' drive. See **Use the plugin** below for some examples on how to use this
-return value in a cross-platform way.
+Read more about `XFile` on the web in
+[`package:cross_file`'s README](https://pub.dev/packages/cross_file).
 
 ### input file "accept"
 
@@ -42,11 +31,26 @@
 Each browser may implement `capture` any way they please, so it may (or may not) make a
 difference in your users' experience.
 
-### pickImage()
-The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported for gif images.
-The argument `imageQuality` only works for jpeg and webp images.
+### input file "cancel"
 
-### pickVideo()
+The [`cancel` event](https://caniuse.com/mdn-api_htmlinputelement_cancel_event)
+used by the plugin to detect when users close the file selector without picking
+a file is relatively new, and will only work in recent browsers.
+
+### `ImagePickerOptions` support
+
+The `ImagePickerOptions` configuration object allows passing resize (`maxWidth`,
+`maxHeight`) and quality (`imageQuality`) parameters to some methods of this
+plugin, which in other platforms control how selected images are resized or
+re-encoded.
+
+On the web:
+
+* `maxWidth`, `maxHeight` and `imageQuality` are not supported for `gif` images.
+* `imageQuality` only affects `jpg` and `webp` images.
+
+### `getVideo()`
+
 The argument `maxDuration` is not supported on the web.
 
 ## Usage
@@ -65,8 +69,8 @@
 
 You should be able to use `package:image_picker` _almost_ as normal.
 
-Once the user has picked a file, the returned `PickedFile` instance will contain a
-`network`-accessible URL (pointing to a location within the browser).
+Once the user has picked a file, the returned `XFile` instance will contain a
+`network`-accessible `Blob` URL (pointing to a location within the browser).
 
 The instance will also let you retrieve the bytes of the selected file across all platforms.
 
diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart
index fffbd6d..5a3af7e 100644
--- a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart
+++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart
@@ -33,7 +33,9 @@
     plugin = ImagePickerPlugin();
   });
 
-  testWidgets('Can select a file (Deprecated)', (WidgetTester tester) async {
+  testWidgets('getImageFromSource can select a file', (
+    WidgetTester _,
+  ) async {
     final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
 
     final ImagePickerPluginTestOverrides overrides =
@@ -44,29 +46,9 @@
     final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);
 
     // Init the pick file dialog...
-    final Future<PickedFile> file = plugin.pickFile();
-
-    // Mock the browser behavior of selecting a file...
-    mockInput.dispatchEvent(html.Event('change'));
-
-    // Now the file should be available
-    expect(file, completes);
-    // And readable
-    expect((await file).readAsBytes(), completion(isNotEmpty));
-  });
-
-  testWidgets('Can select a file', (WidgetTester tester) async {
-    final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
-
-    final ImagePickerPluginTestOverrides overrides =
-        ImagePickerPluginTestOverrides()
-          ..createInputElement = ((_, __) => mockInput)
-          ..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
-
-    final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);
-
-    // Init the pick file dialog...
-    final Future<XFile> image = plugin.getImage(source: ImageSource.camera);
+    final Future<XFile?> image = plugin.getImageFromSource(
+      source: ImageSource.camera,
+    );
 
     // Mock the browser behavior of selecting a file...
     mockInput.dispatchEvent(html.Event('change'));
@@ -75,8 +57,9 @@
     expect(image, completes);
 
     // And readable
-    final XFile file = await image;
-    expect(file.readAsBytes(), completion(isNotEmpty));
+    final XFile? file = await image;
+    expect(file, isNotNull);
+    expect(file!.readAsBytes(), completion(isNotEmpty));
     expect(file.name, textFile.name);
     expect(file.length(), completion(textFile.size));
     expect(file.mimeType, textFile.type);
@@ -87,8 +70,9 @@
         ));
   });
 
-  testWidgets('getMultiImage can select multiple files',
-      (WidgetTester tester) async {
+  testWidgets('getMultiImageWithOptions can select multiple files', (
+    WidgetTester _,
+  ) async {
     final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
 
     final ImagePickerPluginTestOverrides overrides =
@@ -100,7 +84,7 @@
     final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);
 
     // Init the pick file dialog...
-    final Future<List<XFile>> files = plugin.getMultiImage();
+    final Future<List<XFile>> files = plugin.getMultiImageWithOptions();
 
     // Mock the browser behavior of selecting a file...
     mockInput.dispatchEvent(html.Event('change'));
@@ -118,8 +102,7 @@
     expect(secondFile.length(), completion(secondTextFile.size));
   });
 
-  testWidgets('getMedia can select multiple files',
-      (WidgetTester tester) async {
+  testWidgets('getMedia can select multiple files', (WidgetTester _) async {
     final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
 
     final ImagePickerPluginTestOverrides overrides =
@@ -150,7 +133,72 @@
     expect(secondFile.length(), completion(secondTextFile.size));
   });
 
-  // There's no good way of detecting when the user has "aborted" the selection.
+  group('cancel event', () {
+    late html.FileUploadInputElement mockInput;
+    late ImagePickerPluginTestOverrides overrides;
+    late ImagePickerPlugin plugin;
+
+    setUp(() {
+      mockInput = html.FileUploadInputElement();
+      overrides = ImagePickerPluginTestOverrides()
+        ..createInputElement = ((_, __) => mockInput)
+        ..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
+      plugin = ImagePickerPlugin(overrides: overrides);
+    });
+
+    void mockCancel() {
+      mockInput.dispatchEvent(html.Event('cancel'));
+    }
+
+    testWidgets('getFiles - returns empty list', (WidgetTester _) async {
+      final Future<List<XFile>> files = plugin.getFiles();
+      mockCancel();
+
+      expect(files, completes);
+      expect(await files, isEmpty);
+    });
+
+    testWidgets('getMedia - returns empty list', (WidgetTester _) async {
+      final Future<List<XFile>?> files = plugin.getMedia(
+          options: const MediaOptions(
+        allowMultiple: true,
+      ));
+      mockCancel();
+
+      expect(files, completes);
+      expect(await files, isEmpty);
+    });
+
+    testWidgets('getMultiImageWithOptions - returns empty list', (
+      WidgetTester _,
+    ) async {
+      final Future<List<XFile>?> files = plugin.getMultiImageWithOptions();
+      mockCancel();
+
+      expect(files, completes);
+      expect(await files, isEmpty);
+    });
+
+    testWidgets('getImageFromSource - returns null', (WidgetTester _) async {
+      final Future<XFile?> file = plugin.getImageFromSource(
+        source: ImageSource.gallery,
+      );
+      mockCancel();
+
+      expect(file, completes);
+      expect(await file, isNull);
+    });
+
+    testWidgets('getVideo - returns null', (WidgetTester _) async {
+      final Future<XFile?> file = plugin.getVideo(
+        source: ImageSource.gallery,
+      );
+      mockCancel();
+
+      expect(file, completes);
+      expect(await file, isNull);
+    });
+  });
 
   testWidgets('computeCaptureAttribute', (WidgetTester tester) async {
     expect(
@@ -208,4 +256,102 @@
       expect(input.attributes, contains('multiple'));
     });
   });
+
+  group('Deprecated methods', () {
+    late html.FileUploadInputElement mockInput;
+    late ImagePickerPluginTestOverrides overrides;
+    late ImagePickerPlugin plugin;
+
+    setUp(() {
+      mockInput = html.FileUploadInputElement();
+      overrides = ImagePickerPluginTestOverrides()
+        ..createInputElement = ((_, __) => mockInput)
+        ..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
+      plugin = ImagePickerPlugin(overrides: overrides);
+    });
+
+    void mockCancel() {
+      mockInput.dispatchEvent(html.Event('cancel'));
+    }
+
+    void mockChange() {
+      mockInput.dispatchEvent(html.Event('change'));
+    }
+
+    group('getImage', () {
+      testWidgets('can select a file', (WidgetTester _) async {
+        // ignore: deprecated_member_use
+        final Future<XFile?> image = plugin.getImage(
+          source: ImageSource.camera,
+        );
+
+        // Mock the browser behavior when selecting a file...
+        mockChange();
+
+        // Now the file should be available
+        expect(image, completes);
+
+        // And readable
+        final XFile? file = await image;
+        expect(file, isNotNull);
+        expect(file!.readAsBytes(), completion(isNotEmpty));
+        expect(file.name, textFile.name);
+        expect(file.length(), completion(textFile.size));
+        expect(file.mimeType, textFile.type);
+        expect(
+            file.lastModified(),
+            completion(
+              DateTime.fromMillisecondsSinceEpoch(textFile.lastModified!),
+            ));
+      });
+
+      testWidgets('returns null when canceled', (WidgetTester _) async {
+        // ignore: deprecated_member_use
+        final Future<XFile?> file = plugin.getImage(
+          source: ImageSource.gallery,
+        );
+        mockCancel();
+
+        expect(file, completes);
+        expect(await file, isNull);
+      });
+    });
+
+    group('getMultiImage', () {
+      testWidgets('can select multiple files', (WidgetTester _) async {
+        // Override the returned files...
+        overrides.getMultipleFilesFromInput =
+            (_) => <html.File>[textFile, secondTextFile];
+
+        // ignore: deprecated_member_use
+        final Future<List<XFile>> files = plugin.getMultiImage();
+
+        // Mock the browser behavior of selecting a file...
+        mockChange();
+
+        // Now the file should be available
+        expect(files, completes);
+
+        // And readable
+        expect((await files).first.readAsBytes(), completion(isNotEmpty));
+
+        // Peek into the second file...
+        final XFile secondFile = (await files).elementAt(1);
+        expect(secondFile.readAsBytes(), completion(isNotEmpty));
+        expect(secondFile.name, secondTextFile.name);
+        expect(secondFile.length(), completion(secondTextFile.size));
+      });
+
+      testWidgets('returns an empty list when canceled', (
+        WidgetTester _,
+      ) async {
+        // ignore: deprecated_member_use
+        final Future<List<XFile>?> files = plugin.getMultiImage();
+        mockCancel();
+
+        expect(files, completes);
+        expect(await files, isEmpty);
+      });
+    });
+  });
 }
diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
index fb88c96..b54b68a 100644
--- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
+++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
@@ -42,102 +42,47 @@
     ImagePickerPlatform.instance = ImagePickerPlugin();
   }
 
-  /// Returns a [PickedFile] with the image that was picked.
-  ///
-  /// The `source` argument controls where the image comes from. This can
-  /// be either [ImageSource.camera] or [ImageSource.gallery].
-  ///
-  /// Note that the `maxWidth`, `maxHeight` and `imageQuality` arguments are not supported on the web. If any of these arguments is supplied, it'll be silently ignored by the web version of the plugin.
-  ///
-  /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera].
-  /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device.
-  /// Defaults to [CameraDevice.rear].
-  ///
-  /// If no images were picked, the return value is null.
+  /// Returns an [XFile] with the image that was picked, or `null` if no images were picked.
   @override
-  Future<PickedFile> pickImage({
+  Future<XFile?> getImageFromSource({
     required ImageSource source,
-    double? maxWidth,
-    double? maxHeight,
-    int? imageQuality,
-    CameraDevice preferredCameraDevice = CameraDevice.rear,
-  }) {
-    final String? capture =
-        computeCaptureAttribute(source, preferredCameraDevice);
-    return pickFile(accept: _kAcceptImageMimeType, capture: capture);
-  }
-
-  /// Returns a [PickedFile] containing the video that was picked.
-  ///
-  /// The [source] argument controls where the video comes from. This can
-  /// be either [ImageSource.camera] or [ImageSource.gallery].
-  ///
-  /// Note that the `maxDuration` argument is not supported on the web. If the argument is supplied, it'll be silently ignored by the web version of the plugin.
-  ///
-  /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera].
-  /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device.
-  /// Defaults to [CameraDevice.rear].
-  ///
-  /// If no images were picked, the return value is null.
-  @override
-  Future<PickedFile> pickVideo({
-    required ImageSource source,
-    CameraDevice preferredCameraDevice = CameraDevice.rear,
-    Duration? maxDuration,
-  }) {
-    final String? capture =
-        computeCaptureAttribute(source, preferredCameraDevice);
-    return pickFile(accept: _kAcceptVideoMimeType, capture: capture);
-  }
-
-  /// Injects a file input with the specified accept+capture attributes, and
-  /// returns the PickedFile that the user selected locally.
-  ///
-  /// `capture` is only supported in mobile browsers.
-  /// See https://caniuse.com/#feat=html-media-capture
-  @visibleForTesting
-  Future<PickedFile> pickFile({
-    String? accept,
-    String? capture,
-  }) {
-    final html.FileUploadInputElement input =
-        createInputElement(accept, capture) as html.FileUploadInputElement;
-    _injectAndActivate(input);
-    return _getSelectedFile(input);
-  }
-
-  /// Returns an [XFile] with the image that was picked.
-  ///
-  /// The `source` argument controls where the image comes from. This can
-  /// be either [ImageSource.camera] or [ImageSource.gallery].
-  ///
-  /// Note that the `maxWidth`, `maxHeight` and `imageQuality` arguments are not supported on the web. If any of these arguments is supplied, it'll be silently ignored by the web version of the plugin.
-  ///
-  /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera].
-  /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device.
-  /// Defaults to [CameraDevice.rear].
-  ///
-  /// If no images were picked, the return value is null.
-  @override
-  Future<XFile> getImage({
-    required ImageSource source,
-    double? maxWidth,
-    double? maxHeight,
-    int? imageQuality,
-    CameraDevice preferredCameraDevice = CameraDevice.rear,
+    ImagePickerOptions options = const ImagePickerOptions(),
   }) async {
     final String? capture =
-        computeCaptureAttribute(source, preferredCameraDevice);
+        computeCaptureAttribute(source, options.preferredCameraDevice);
     final List<XFile> files = await getFiles(
       accept: _kAcceptImageMimeType,
       capture: capture,
     );
-    return _imageResizer.resizeImageIfNeeded(
-      files.first,
-      maxWidth,
-      maxHeight,
-      imageQuality,
+    return files.isEmpty
+        ? null
+        : _imageResizer.resizeImageIfNeeded(
+            files.first,
+            options.maxWidth,
+            options.maxHeight,
+            options.imageQuality,
+          );
+  }
+
+  /// Returns a [List<XFile>] with the images that were picked, if any.
+  @override
+  Future<List<XFile>> getMultiImageWithOptions({
+    MultiImagePickerOptions options = const MultiImagePickerOptions(),
+  }) async {
+    final List<XFile> images = await getFiles(
+      accept: _kAcceptImageMimeType,
+      multiple: true,
     );
+    final Iterable<Future<XFile>> resized = images.map(
+      (XFile image) => _imageResizer.resizeImageIfNeeded(
+        image,
+        options.imageOptions.maxWidth,
+        options.imageOptions.maxHeight,
+        options.imageOptions.imageQuality,
+      ),
+    );
+
+    return Future.wait<XFile>(resized);
   }
 
   /// Returns an [XFile] containing the video that was picked.
@@ -153,7 +98,7 @@
   ///
   /// If no images were picked, the return value is null.
   @override
-  Future<XFile> getVideo({
+  Future<XFile?> getVideo({
     required ImageSource source,
     CameraDevice preferredCameraDevice = CameraDevice.rear,
     Duration? maxDuration,
@@ -164,30 +109,7 @@
       accept: _kAcceptVideoMimeType,
       capture: capture,
     );
-    return files.first;
-  }
-
-  /// Injects a file input, and returns a list of XFile images that the user selected locally.
-  @override
-  Future<List<XFile>> getMultiImage({
-    double? maxWidth,
-    double? maxHeight,
-    int? imageQuality,
-  }) async {
-    final List<XFile> images = await getFiles(
-      accept: _kAcceptImageMimeType,
-      multiple: true,
-    );
-    final Iterable<Future<XFile>> resized = images.map(
-      (XFile image) => _imageResizer.resizeImageIfNeeded(
-        image,
-        maxWidth,
-        maxHeight,
-        imageQuality,
-      ),
-    );
-
-    return Future.wait<XFile>(resized);
+    return files.isEmpty ? null : files.first;
   }
 
   /// Injects a file input, and returns a list of XFile media that the user selected locally.
@@ -239,6 +161,58 @@
     return _getSelectedXFiles(input);
   }
 
+  // Deprecated methods follow...
+
+  /// Returns an [XFile] with the image that was picked.
+  ///
+  /// The `source` argument controls where the image comes from. This can
+  /// be either [ImageSource.camera] or [ImageSource.gallery].
+  ///
+  /// Note that the `maxWidth`, `maxHeight` and `imageQuality` arguments are not supported on the web. If any of these arguments is supplied, it'll be silently ignored by the web version of the plugin.
+  ///
+  /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera].
+  /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device.
+  /// Defaults to [CameraDevice.rear].
+  ///
+  /// If no images were picked, the return value is null.
+  @override
+  @Deprecated('Use getImageFromSource instead.')
+  Future<XFile?> getImage({
+    required ImageSource source,
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+    CameraDevice preferredCameraDevice = CameraDevice.rear,
+  }) async {
+    return getImageFromSource(
+        source: source,
+        options: ImagePickerOptions(
+          maxWidth: maxWidth,
+          maxHeight: maxHeight,
+          imageQuality: imageQuality,
+          preferredCameraDevice: preferredCameraDevice,
+        ));
+  }
+
+  /// Injects a file input, and returns a list of XFile images that the user selected locally.
+  @override
+  @Deprecated('Use getMultiImageWithOptions instead.')
+  Future<List<XFile>> getMultiImage({
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+  }) async {
+    return getMultiImageWithOptions(
+      options: MultiImagePickerOptions(
+        imageOptions: ImageOptions(
+          maxWidth: maxWidth,
+          maxHeight: maxHeight,
+          imageQuality: imageQuality,
+        ),
+      ),
+    );
+  }
+
   // DOM methods
 
   /// Converts plugin configuration into a proper value for the `capture` attribute.
@@ -267,29 +241,6 @@
     return input == null ? null : _getFilesFromInput(input);
   }
 
-  /// Monitors an <input type="file"> and returns the selected file.
-  Future<PickedFile> _getSelectedFile(html.FileUploadInputElement input) {
-    final Completer<PickedFile> completer = Completer<PickedFile>();
-    // Observe the input until we can return something
-    input.onChange.first.then((html.Event event) {
-      final List<html.File>? files = _handleOnChangeEvent(event);
-      if (!completer.isCompleted && files != null) {
-        completer.complete(PickedFile(
-          html.Url.createObjectUrl(files.first),
-        ));
-      }
-    });
-    input.onError.first.then((html.Event event) {
-      if (!completer.isCompleted) {
-        completer.completeError(event);
-      }
-    });
-    // Note that we don't bother detaching from these streams, since the
-    // "input" gets re-created in the DOM every time the user needs to
-    // pick a file.
-    return completer.future;
-  }
-
   /// Monitors an <input type="file"> and returns the selected file(s).
   Future<List<XFile>> _getSelectedXFiles(html.FileUploadInputElement input) {
     final Completer<List<XFile>> completer = Completer<List<XFile>>();
@@ -310,6 +261,11 @@
         }).toList());
       }
     });
+
+    input.addEventListener('cancel', (html.Event _) {
+      completer.complete(<XFile>[]);
+    });
+
     input.onError.first.then((html.Event event) {
       if (!completer.isCompleted) {
         completer.completeError(event);
@@ -361,6 +317,7 @@
   void _injectAndActivate(html.Element element) {
     _target.children.clear();
     _target.children.add(element);
+    // TODO(dit): Reimplement this with the showPicker() API, https://github.com/flutter/flutter/issues/130365
     element.click();
   }
 }
diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml
index a61a5b8..d7e274b 100644
--- a/packages/image_picker/image_picker_for_web/pubspec.yaml
+++ b/packages/image_picker/image_picker_for_web/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Web platform implementation of image_picker
 repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_for_web
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
-version: 2.2.0
+version: 3.0.0
 
 environment:
   sdk: ">=2.18.0 <4.0.0"
@@ -21,7 +21,7 @@
     sdk: flutter
   flutter_web_plugins:
     sdk: flutter
-  image_picker_platform_interface: ^2.8.0
+  image_picker_platform_interface: ^2.9.0
   mime: ^1.0.4
 
 dev_dependencies: