Skip to content

[Bug]: Avoid crash on iOS 26 on iPhones with no sharePositionOrigin #3768

@LuisGabrielZamora

Description

@LuisGabrielZamora

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions