-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
Platform
Ipad - IOS From 17 to 26
Plugin
share_plus
Version
12.0.1
Flutter SDK
3.38.3
Steps to reproduce
Description
Fixes share sheet on iPad (iOS 17–26) when sharePositionOrigin was missing or in the wrong coordinate space.
Context
This problem is related with this the following previous bugs, they tried to fix the problem but this is still occurring:
Why the previous code didn’t work (iPad)
1. Wrong coordinate space
Flutter sends sharePositionOrigin in global (window) coordinates (from localToGlobal()). The old code used that rect directly as sourceRect. On iPad, UIPopoverPresentationController.sourceRect must be in the source view’s coordinate space. Using window coordinates as-is broke the containment check and could misplace the popover or trigger validation failures.
2. Over-strict validation
The code required CGRectContainsRect(controller.view.frame, origin) and a non-empty origin. Because origin is in window coordinates and controller.view.frame is in another space, the check often failed even with a valid rect. When the app didn’t pass sharePositionOrigin (e.g. CGRectZero), it returned a FlutterError and blocked sharing instead of using a fallback.
3. Result
On iPad (iOS 17–26), share could fail with a platform error or the popover could anchor in the wrong place. iPhone was unaffected because it doesn’t rely on the same popover path.
Changes
iPad – coordinate handling: Convert sharePositionOrigin from window to the source view with [controller.view convertRect:origin fromView:nil] before setting sourceRect, so the popover anchors correctly.
iPad – no origin: If no valid origin is provided, use a fallback rect at the center of the controller’s view instead of returning a FlutterError, so sharing always works on iPad. iPhone: Unchanged; sourceRect is still set only when a non-empty origin is provided.
Backwards compatibility
- iPhone: Unchanged.
- iPad without sharePositionOrigin: Previously could fail; now works (center fallback).
- iPad with sharePositionOrigin: Popover now anchors correctly; no API changes.
Code Sample
// ignore_for_file: unused_element, deprecated_member_use
// =============================================================================
// STANDALONE QA FILE — NO ABCMOUSE DEPENDENCIES
// =============================================================================
// Copy the entire folder (this file + pubspec.yaml) outside the project to run
// tests in isolation. No references to abcmouse, BaseButton, or getIt.
//
// 1. Copy folder to e.g. /tmp/share_with_apps_qa/
// 2. cd /tmp/share_with_apps_qa && flutter pub get && dart test .
//
// Run: dart test share_with_apps_isolated_qa.dart
// =============================================================================
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:test/test.dart';
// -----------------------------------------------------------------------------
// 1. SHARE WITH APPS — ABSTRACT & IMPLEMENTATION
// -----------------------------------------------------------------------------
/// Abstract interface for sharing text/files and the activation link.
abstract class ShareWithApps {
Future<void> getShareTextWithApps(String text, String shareMessage);
Future<void> getShareFileWithApps(String filePath, String shareMessage);
Future<bool> getShareFileFromUint8ListWithApps(
Uint8List uint8List,
String shareMessage,
);
Future<void> shareLinkWithApps(String subject);
}
/// Implementation that uses SharePlus and an injected URL provider.
class ShareWithAppsImpl implements ShareWithApps {
ShareWithAppsImpl({
required Future<String> Function() getActivationUrl,
void Function(String message, [Object? error, StackTrace? stackTrace])? log,
}) : _getActivationUrl = getActivationUrl,
_log = log ?? _noopLog;
final Future<String> Function() _getActivationUrl;
final void Function(String, [Object?, StackTrace?]) _log;
@override
Future<void> getShareTextWithApps(String text, String shareMessage) async {
final result = await SharePlus.instance.share(
ShareParams(text: text, subject: shareMessage),
);
if (result.status != ShareResultStatus.success) {
_log('Share text failed: ${result.status}');
}
}
@override
Future<void> getShareFileWithApps(
String filePath,
String shareMessage,
) async {
final file = XFile(filePath);
final result = await SharePlus.instance.share(
ShareParams(
files: [file],
subject: shareMessage,
downloadFallbackEnabled: false,
),
);
if (result.status != ShareResultStatus.success) {
_log('Share file failed: ${result.status}');
}
}
@override
Future<bool> getShareFileFromUint8ListWithApps(
Uint8List uint8List,
String shareMessage,
) async {
final file = XFile.fromData(
uint8List,
name: 'qrcode.png',
mimeType: 'image/png',
);
try {
final result = await SharePlus.instance.share(
ShareParams(
files: [file],
subject: shareMessage,
downloadFallbackEnabled: false,
),
);
return result.status == ShareResultStatus.success;
} catch (e, st) {
if (kIsWeb) {
_log('Share file unavailable on web', e, st);
return false;
}
rethrow;
}
}
@override
Future<void> shareLinkWithApps(String subject) async {
final url = await _getActivationUrl();
await getShareTextWithApps(url, subject);
}
}
// -----------------------------------------------------------------------------
// 2. TEMPORARY FILE HELPER
// -----------------------------------------------------------------------------
void _noopLog(String message, [Object? error, StackTrace? st]) {}
Future<File?> createTemporaryFileFromUint8List(
Uint8List bytes,
String fileName, {
void Function(String message, [Object? error, StackTrace? stackTrace])? log,
}) async {
final logFn = log ?? _noopLog;
try {
final tempDir = await getTemporaryDirectory();
final tempPath = '${tempDir.path}/$fileName.png';
final file = File(tempPath);
await file.writeAsBytes(bytes);
return file;
} catch (e, st) {
logFn('Error creating temporary file: $e', e, st);
return null;
}
}
// -----------------------------------------------------------------------------
// 3. SHARE LINK ENTRY POINT (testable without Flutter)
// -----------------------------------------------------------------------------
Future<void> shareLinkWithAppsSubject(
ShareWithApps shareWithApps,
String subject,
) async {
await shareWithApps.shareLinkWithApps(subject);
}
// -----------------------------------------------------------------------------
// 4. STANDALONE BUTTON — PLAIN FLUTTER, NO BASE BUTTON / ANALYTICS
// -----------------------------------------------------------------------------
/// Button that opens the share sheet with the activation link.
/// Uses only Flutter Material widgets; no project-specific dependencies.
class ShareWithAppsButton extends StatefulWidget {
const ShareWithAppsButton({
super.key,
required this.subject,
required this.child,
this.shareWithApps,
});
final String subject;
final Widget child;
final ShareWithApps? shareWithApps;
@override
State<ShareWithAppsButton> createState() => _ShareWithAppsButtonState();
}
class _ShareWithAppsButtonState extends State<ShareWithAppsButton> {
bool _loading = false;
late ShareWithApps _shareWithApps;
@override
void initState() {
super.initState();
_shareWithApps = widget.shareWithApps ?? ShareWithAppsImpl(
getActivationUrl: () async => 'https://example.com/activate',
log: (msg, [e, st]) => debugPrint('$msg $e $st'),
);
}
@override
void didUpdateWidget(ShareWithAppsButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.shareWithApps != null) {
_shareWithApps = widget.shareWithApps!;
}
}
Future<void> _onPressed() async {
if (_loading) return;
if (!mounted) return;
setState(() => _loading = true);
try {
await shareLinkWithAppsSubject(_shareWithApps, widget.subject);
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return Material(
child: InkWell(
onTap: _loading ? null : _onPressed,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: _loading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: widget.child,
),
),
);
}
}
// -----------------------------------------------------------------------------
// 5. FAKE FOR TESTS
// -----------------------------------------------------------------------------
class FakeShareWithApps implements ShareWithApps {
final List<String> shareLinkSubjects = [];
final List<(String, String)> shareTextCalls = [];
@override
Future<void> getShareTextWithApps(String text, String shareMessage) async {
shareTextCalls.add((text, shareMessage));
}
@override
Future<void> getShareFileWithApps(
String filePath,
String shareMessage,
) async {}
@override
Future<bool> getShareFileFromUint8ListWithApps(
Uint8List uint8List,
String shareMessage,
) async =>
true;
@override
Future<void> shareLinkWithApps(String subject) async {
shareLinkSubjects.add(subject);
}
}
// -----------------------------------------------------------------------------
// 6. TESTS
// -----------------------------------------------------------------------------
void main() {
group('shareLinkWithAppsSubject', () {
test('calls shareLinkWithApps on the given implementation with the subject',
() async {
final fake = FakeShareWithApps();
await shareLinkWithAppsSubject(fake, 'Copy Link');
expect(fake.shareLinkSubjects, ['Copy Link']);
});
test('can be called multiple times with different subjects', () async {
final fake = FakeShareWithApps();
await shareLinkWithAppsSubject(fake, 'Copy Link');
await shareLinkWithAppsSubject(fake, 'Share QR Code');
expect(fake.shareLinkSubjects, ['Copy Link', 'Share QR Code']);
});
});
group('FakeShareWithApps', () {
test('records shareLinkWithApps subjects', () async {
final fake = FakeShareWithApps();
await fake.shareLinkWithApps('Subject A');
await fake.shareLinkWithApps('Subject B');
expect(fake.shareLinkSubjects, ['Subject A', 'Subject B']);
});
test('records getShareTextWithApps text and message', () async {
final fake = FakeShareWithApps();
await fake.getShareTextWithApps('https://x.com', 'Share this');
expect(fake.shareTextCalls, [('https://x.com', 'Share this')]);
});
});
}Logs
[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: PlatformException(error, sharePositionOrigin: argument must be set, {{0, 0}, {0, 0}} must be non-zero and within coordinate space of source view: {{0, 0}, {414, 896}}, null, null)
#0 StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:653:7)
#1 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:367:18)
<asynchronous suspension>
#2 MethodChannelShare.share (package:share_plus_platform_interface/method_channel/method_channel_share.dart:25:20)
<asynchronous suspension>Flutter Doctor
[✓] Flutter (Channel stable, 3.38.3, on macOS 26.3 25D125 darwin-arm64, locale en-MX) [562ms]
• Flutter version 3.38.3 on channel stable at /Users/luis.acevedo/fvm/versions/3.38.3
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 19074d12f7 (3 months ago), 2025-11-20 17:53:13 -0500
• Engine revision 13e658725d
• Dart version 3.10.1
• DevTools version 2.51.1
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations,
enable-native-assets, omit-legacy-version-file, enable-lldb-debugging
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0) [3.0s]
• Android SDK at /Users/luis.acevedo/Library/Android/sdk
• Emulator version unknown
• Platform android-36, build-tools 35.0.0
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 26.2) [1,220ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 17C52
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [50ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (4 available) [6.6s]
• iPad 6 (mobile) • 2b0db79e161db6f815a9cfdbf815884404fcfb6f • ios • iOS 17.7.10 21H450
• iPhone (mobile) • 00008140-000951CA3662801C • ios • iOS 26.3 23D127
• macOS (desktop) • macos • darwin-arm64 • macOS 26.3 25D125 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 145.0.7632.117
[✓] Network resources [587ms]
• All expected network resources are available.
• No issues found!Checklist before submitting a bug
- I searched issues in this repository and couldn't find such bug/problem
- I Google'd a solution and I couldn't find it
- I searched on StackOverflow for a solution and I couldn't find it
- I read the README.md file of the plugin
- I'm using the latest version of the plugin
- All dependencies are up to date with
flutter pub upgrade - I did a
flutter clean - I tried running the example project