The share sheet, with answers.#
Native Share opens the real OS share sheet — and then tells you what happened. You'll know whether the user actually shared, and which app they picked. It also covers the cases the free plugin doesn't: several files in one sheet, links with rich previews, hiding apps you don't want listed, and skipping the sheet to go straight to one app on Android.
✅ Tested on real iOS and Android devices (2026-10-04).
▶️ Demo (Android, 90 s): watch on YouTube: share result, link preview, several files, hiding apps, and the fallback when the target app isn't installed.
Used in: PARqore's own apps New Life Daily, FarmSeconds, and Hard Moments.
See it in action#
Vipul Walia (vipertecpro) built a free, open-source demo app and a hands-on walkthrough of this plugin, together with PARqore's Speech to Text, Text to Speech and Force Update:
- Video: NativePHP Mobile: Speech to Text, Text to Speech, Share & Force Update
- Demo app on GitHub
- Hands-on blog post
How this differs from the official plugin#
The official nativephp/mobile-share is free and covers simple URL/file sharing —
start there if that's all you need. Reach for Native Share when you need the rest:
a ShareCompleted event reporting whether the share finished and which app was picked,
multi-file sharing in a single sheet, rich link previews, exclude to hide apps from
the sheet, and target to send straight to one Android app. (copy() is deprecated
in 2.0 and still works until 3.0 — the official nativephp/mobile-clipboard handles
the clipboard.)
Brand assets for local packaging live in docs/brand and art (private repo paths are not embedded as README images so the marketplace listing does not show broken image placeholders).
Overview#
share($title, $text): plain text, with the title as the sheet header and email subject.shareUrl($url): a link. iOS shows a rich preview (LPLinkMetadata) with your title and image; Android 10+ shows them in the chooser header.shareFiles($paths): one or more photos, videos, PDFs or other files in one sheet (ACTION_SEND_MULTIPLEon Android).- Every share returns an id, and a
ShareCompletedevent reports what happened: completed or not, and which app. - Options on every share:
excludehides apps from the sheet, andtargetsends straight to one Android app, with the normal sheet as the fallback.
The share sheet is the OS app picker. Whatever app the user picks (Messages, WhatsApp, Mail) brings its own recipient picker, so there's no in-app contact picker here.
copy() is deprecated in 2.0 and will be removed in 3.0. Use nativephp/mobile-clipboard instead (see Upgrading from 1.x).
Installation#
The repository is private. Add it to your app's composer.json as a VCS repository:
"repositories": [ { "type": "vcs", "url": "https://github.com/prosenthal828/nativephp-native-share" }]
Use the VCS URL rather than a local path repository so Bifrost and CI builds can resolve the package. Those machines need GitHub read access to the repo (for example through COMPOSER_AUTH).
Then:
composer require partek/native-sharephp artisan native:plugin:register partek/native-share
native:plugin:register adds the service provider to NativeServiceProvider::plugins(), which native:run reads at build time. Rebuild the native projects afterwards (php artisan native:run android or ios). There are no permissions to add.
Usage#
PHP (Super Native / Livewire / Blade)#
The facade is for Super Native first; the same calls work from Livewire and Blade.
use Partek\NativeShare\Facades\NativeShare; // Text$id = NativeShare::share('Invite', 'Join me on FarmSeconds: https://farmseconds.com/invite/abc'); // A link with a rich preview$id = NativeShare::shareUrl('https://farmseconds.com/listing/42', [ 'title' => 'Fresh eggs, 2 miles away', 'image' => storage_path('app/listing-42.jpg'),]); // Several files in one sheet$id = NativeShare::shareFiles([ storage_path('app/photos/1.jpg'), storage_path('app/photos/2.jpg'), storage_path('app/receipt.pdf'),], ['title' => 'Order #1042', 'text' => 'Photos and the receipt']); // Hide apps, or go straight to one (Android)NativeShare::share('Invite', $text, ['exclude' => ['com.facebook.katana', 'print']]);NativeShare::share('Invite', $text, ['target' => 'com.whatsapp']);
Each method returns the share id that ShareCompleted carries.
Listening for the result#
use Native\Mobile\Attributes\On;use Partek\NativeShare\Events\ShareCompleted; #[On(ShareCompleted::class)]public function shareCompleted(string $id, bool $completed, ?string $app = null): void{ if ($id === $this->shareId && $completed) { $this->status = "Shared with {$app}"; }}
In Livewire, listen with #[On('native:Partek\\NativeShare\\Events\\ShareCompleted')].
JavaScript#
The plugin ships its JavaScript at resources/js/index.js. NativePHP doesn't alias plugin JavaScript for you, so add an alias in vite.config.js:
import { fileURLToPath } from 'node:url'; export default defineConfig({ resolve: { alias: { 'partek-native-share': fileURLToPath( new URL('./vendor/partek/native-share/resources/js/index.js', import.meta.url) ), }, }, // ...});
import { share, shareUrl, shareFiles } from 'partek-native-share'; const id = await share('Invite', 'Join me: https://farmseconds.com/invite/abc', { exclude: ['print'] });await shareUrl('https://farmseconds.com/listing/42', { title: 'Fresh eggs' });await shareFiles(['/path/to/1.jpg', '/path/to/2.jpg'], { target: 'com.whatsapp' });
Each resolves with the share id and rejects with an Error if the bridge returns one. Without the alias, core's bridgeCall from #nativephp works too: bridgeCall('NativeShare.Share', { id, title, text }).
Events#
Partek\NativeShare\Events\ShareCompleted#
Sent once the share sheet is settled.
| Property | Type | Description |
|---|---|---|
id |
string | The id the share method returned |
completed |
bool | iOS: the picked app finished the share. Android: the user picked an app. |
app |
?string | iOS: the activity type (com.apple.UIKit.activity.Mail, net.whatsapp.WhatsApp.ShareExtension, ...). Android: the package (com.google.android.gm, ...). null when nothing was picked. |
What each platform can see:
- iOS reports through
completionWithItemsHandler. If the user opens an app's share screen and backs out, iOS returns to the sheet; the event comes once the sheet itself is closed. - Android reports the app the user picked (
EXTRA_CHOSEN_COMPONENT) and can't see what happens inside it, socompleted: truemeans "an app was picked". The event arrives when the user is back in your app. The chooser's own Copy button isn't an app, so it reportscompleted: false. Share targets built into the phone, like Samsung Quick Share, are apps and report their package. targetopens the app with no sheet, so there's nothing to pick: the event sayscompleted: truewith that package as soon as the app opens.
Methods#
share(string $title, string $text, array $options = []): string#
| Parameter | Type | Description |
|---|---|---|
title |
string | Sheet header and email subject. It isn't added to the message body. Pass '' for the system's default header. |
text |
string | The text to share |
options |
array | exclude, target (see Options) |
shareUrl(string $url, array $options = []): string#
| Option | Type | Description |
|---|---|---|
title |
string | Preview title and email subject |
text |
string | Text sent before the link |
image |
string | Absolute path to a local image for the preview (iOS preview image and icon, Android 10+ chooser thumbnail) |
$url must be absolute (https://...). Throws InvalidArgumentException otherwise, or when image isn't a readable file.
shareFiles(array $paths, array $options = []): string#
| Option | Type | Description |
|---|---|---|
title |
string | Sheet header and email subject |
text |
string | Text sent with the files, where the picked app accepts it |
$paths are absolute paths to files the app can read, such as storage_path(...). Throws InvalidArgumentException for an empty list or a missing or unreadable file.
Options#
Every share method takes these:
| Option | Type | Description |
|---|---|---|
exclude |
string[] | Apps to hide. Android: package names (com.facebook.katana). iOS: system activities by name (airDrop, print, copy, mail, message, assignToContact, saveToCameraRoll, addToReadingList, markupAsPDF, openInIBooks) or a raw activity type. Each platform ignores the entries that aren't its own, so one list serves both. |
target |
string | Android package to open directly, such as com.whatsapp. If it isn't installed (or can't take this share), the normal sheet opens. iOS has no API for this and always shows the sheet. |
iOS can only hide system activities. Apple doesn't let apps hide other apps' share extensions.
copy(string $text): void (deprecated)#
Writes $text to the system clipboard. Deprecated since 2.0.0 and removed in 3.0: use nativephp/mobile-clipboard.
Upgrading from 1.x#
-
share($title, $text)works as before. It now returns the share id instead ofvoid, and the sheet reportsShareCompleted. -
copy()still works in 2.x but is deprecated. Switch tonativephp/mobile-clipboard:Copied!composer require nativephp/mobile-clipboardphp artisan native:plugin:register nativephp/mobile-clipboardCopied!use NativePHP\Clipboard\Facades\Clipboard;Clipboard::writeText('https://farmseconds.com/invite/abc'); -
The Android build gains a
<queries>entry forACTION_SEND/ACTION_SEND_MULTIPLE(added by the plugin'spost_compilehook), soexcludecan find share targets on Android 11+. It doesn't useQUERY_ALL_PACKAGES.
Platform behavior#
Android#
- Text and links:
Intent.ACTION_SEND(text/plain) throughIntent.createChooser. The title goes intoEXTRA_TITLE(the sheet header on Android 10+),EXTRA_SUBJECT, and the chooser title on older versions. A link is sent as text after anytext. - Files: copied into the app's cache (
cache/native-share/<id>, removed after an hour) and shared through core'sFileProviderwith a read grant. One file isACTION_SEND, several areACTION_SEND_MULTIPLE, with the MIME type narrowed to what they share (image/*, or*/*for a mix). - Link preview image: set as the intent's
ClipData, which Android 10+ shows in the chooser header. - Result: a
PendingIntentbroadcast withEXTRA_CHOSEN_COMPONENT, received by a non-exported receiver. exclude:EXTRA_EXCLUDE_COMPONENTS(Android 7+).target:Intent.setPackage(), falling back to the chooser when no activity matches.- Min SDK 21
iOS#
UIActivityViewController, presented from the topmost view controller so an open modal doesn't block it. On iPad it's anchored to the center of the screen.- Text: the title becomes the sheet header (
LPLinkMetadata) and the Mail subject. - Links:
LPLinkMetadatawith the URL, title and image. - Files: one activity item per file URL; the first one sets the sheet header.
- Result:
completionWithItemsHandler.exclude:excludedActivityTypes. - Min iOS 15.0
Testing#
Test helpers#
On NativePHP Mobile 4.0 or later, the plugin registers these helpers on Native::fakeBridge() and Native::test() whenever the app runs under a test runner (APP_ENV=testing). Production boots never register them, and there is nothing extra to install.
use App\NativeComponents\ListingScreen;use Native\Mobile\Testing\Native; Native::test(ListingScreen::class) ->tap('Share listing') ->assertUrlShared('https://farmseconds.com/listing/42', 'Fresh eggs, 2 miles away') ->assertShareExcluded('com.facebook.katana') ->completeShare('com.whatsapp') ->assertSee('Shared with com.whatsapp'); Native::test(ListingScreen::class) ->tap('Share listing') ->cancelShare() ->assertSee('Not shared');
| Helper | Called on | What it does |
|---|---|---|
assertTextShared(?string $title = null, ?string $text = null) |
bridge | share() reached the bridge: any call, or one call with exactly the $title and/or $text you pass. '' matches share('', …). |
assertTextNotShared() |
bridge | Nothing was shared (no share(), shareUrl() or shareFiles()). |
assertUrlShared(?string $url = null, ?string $title = null) |
bridge | shareUrl() reached the bridge: any link, or exactly $url (and $title). |
assertFilesShared(?array $paths = null) |
bridge | shareFiles() reached the bridge: any files, or exactly these paths in this order. |
assertShareExcluded(string ...$apps) |
bridge | A share hid all of these apps. |
assertShareTargeted(string $package) |
bridge | A share went straight to $package. |
assertNativeShared(?string $title = null, ?string $text = null) |
bridge | The same check as assertTextShared(). |
assertShared(?string $title = null, ?string $text = null) |
bridge | The 1.1 name for assertTextShared(). Registered only when no other plugin has registered assertShared(); see below. |
assertCopied(?string $text = null), assertNothingCopied() |
bridge | For the deprecated copy(). |
completeShare(?string $app = 'com.example.messenger', ?string $id = null) |
screen | Delivers ShareCompleted with completed: true, for the latest share unless you pass $id. |
cancelShare(?string $id = null) |
screen | Delivers ShareCompleted with completed: false and no app. |
Bridge helpers work on Native::fakeBridge() or straight off the harness. Called off the harness, they return the harness, so the chain continues.
- Nothing to script. The share methods never read the bridge's answer, so the
nullthe fake bridge returns by default doesn't change what your screen does. assertShared()andnativephp/mobile-share. The official share plugin registers its ownassertShared(), which checks itsShare.UrlandShare.Filecalls. Macros are global and the last registration wins. This plugin only registersassertShared()when the name is free, so with both installedassertShared()is the official plugin's.assertTextShared()andassertNativeShared()are always this plugin's, so use one of those in new tests.- Mocked facade. If you mock the facade (
NativeShare::shouldReceive(...)), calls never reach the bridge, so these helpers see nothing.
This package's suite#
composer installvendor/bin/phpunit
NativeShareTest checks the plugin with no bridge at all and against a bare FakeBridge, plus the manifest, the event payload keys both platforms send, and the post_compile hook. NativeShareMacrosTest boots the NativePHP core with Testbench and runs the test helpers through the real facade and a small fixture screen.
Notes#
- No permissions on either platform.
- Register the plugin and rebuild, or the bridge functions and the
post_compilehook never run. - Receiving shares from other apps isn't part of this plugin.
License#
Proprietary commercial software. See LICENSE.