Share Sheet for NativePHP Mobile#
Let people share links, text, photos and files into your NativePHP app from any other app, on iOS and Android.
- iOS: a real Share Extension. Your app sits in the system share sheet like any other app. Nobody installs a Shortcut, and you write no Swift.
- Android: your app appears in the share menu for links, text, photos and files.
- One API on both.
ShareSheet::pending()in PHP,pending()in JavaScript. - People stay where they were. A short "Saved to Your App" confirmation appears over the app they shared from, and your app collects everything next time it opens.
use Firsthand\ShareSheet\Facades\ShareSheet; foreach (ShareSheet::pending() as $item) { match (true) { $item->isUrl() => Bookmark::create(['url' => $item->url()]), $item->isText() => Note::create(['body' => $item->text()]), $item->hasFile() => Attachment::create(['path' => $item->store('inbox')]), };}
Tested on real devices: iPad (TestFlight build) and an Android phone: links, text, photos and files. See CHANGELOG.md in the package.
Contents: Requirements · Installation · Apple setup · PHP · Livewire · Vue & React · Configuration · Doctor · Testing · Good to know · Troubleshooting
Requirements#
- NativePHP Mobile 4.x, Laravel 11–13, PHP 8.3+
- iOS 18+: an Apple Developer account (App Groups are a paid-account feature)
- Android 10+ (API 29): no extra setup
- Livewire 3 or 4, or Inertia with Vue or React (all four are tested)
Installation#
composer require firsthand/nativephp-share-sheetphp artisan native:plugin:register firsthand/nativephp-share-sheet
Every iOS and Android build now includes the share target. There's nothing to add to Xcode or Android Studio.
One-time Apple setup (iOS only)#
On iOS the share target is a small second program inside your app, and it passes what it receives to your app through an App Group. You register the group yourself, once. It takes about 3 minutes, on developer.apple.com → Identifiers:
- Register the App Group. Click +, choose App Groups, and enter your
NATIVEPHP_APP_ID(for examplecom.example.myapp). The form adds thegroup.prefix itself, so don't type it. - Your app's ID (
com.example.myapp): tick App Groups, click Configure, choose the group, then Save. - The extension's ID (
com.example.myapp.ShareExtension): your first signed build registers it. Open it and give it the same App Group.
Builds signed with an App Store Connect API key (CI, Bifrost) can register app IDs but cannot create App Groups. Do step 1 by hand before your first CI build, or signing fails with "No profiles … were found".
To use a different group name, set SHARE_SHEET_APP_GROUP=group.something.else in .env.
Using it from PHP#
use Firsthand\ShareSheet\Facades\ShareSheet; $items = ShareSheet::pending(); // takes everything waiting; each item is handed over once$items = ShareSheet::peek(); // looks without takingShareSheet::hasPending(); // boolShareSheet::status(); // setup check on the device, see below
Each item is a Firsthand\ShareSheet\SharedItem:
type |
url, text, image or file |
isUrl() isText() isImage() isFile() |
|
hasFile() |
true for photos and files |
url() / text() |
the link or the words (null otherwise) |
title |
for links: the page title, when the sharing app sends one (Android browsers do) |
filename, mime, size, sharedAt |
for photos and files (sharedAt for everything) |
store($dir = 'shared', $disk = null) |
moves a photo or file onto a Laravel disk and returns its path |
contents() |
the raw bytes |
forget() |
discards a photo or file you don't want |
toArray() |
plain array, for JSON or Livewire properties |
Photos and files arrive as temporary copies. store() or forget() each one.
Outside the app (in the browser during development) pending() returns an empty collection, so your pages still render. A real setup problem, such as the App Group not being registered, is thrown with the fix in the message rather than silently returning nothing.
Reacting when someone comes back from sharing#
The app fires Firsthand\ShareSheet\Events\SharesReceived (with a count) when it comes to the front with shares waiting.
Livewire#
use Firsthand\ShareSheet\Events\SharesReceived;use Firsthand\ShareSheet\Facades\ShareSheet;use Livewire\Component;use Native\Mobile\Attributes\OnNative; class ShareInbox extends Component{ public array $items = []; public function mount(): void { $this->collect(); // something may have been shared before this screen opened } #[OnNative(SharesReceived::class)] public function collect(): void { foreach (ShareSheet::pending() as $item) { $this->items[] = [ 'type' => $item->type, 'label' => $item->url() ?? $item->text() ?? $item->filename, 'stored' => $item->hasFile() ? $item->store('shared') : null, ]; } }}
The complete component and view are in examples/livewire. Livewire properties hold plain arrays, so store files and keep arrays rather than SharedItem objects.
Inertia (Vue & React)#
The JavaScript API ships inside the package:
npm install ./vendor/firsthand/nativephp-share-sheet
import { pending, peek, status, onSharesReceived } from '@firsthand/nativephp-share-sheet'; const items = await pending(); // same items, same "handed over once" ruleconst stop = onSharesReceived(async ({ count }) => { const fresh = await pending();});stop(); // on unmount
TypeScript types are included. Ready-to-copy components: examples/vue/ShareInbox.vue and examples/react/ShareInbox.jsx.
Photos and files arrive with a path on the device. To keep one, store it from PHP (SharedItem::store) or upload it to your server.
Configuration#
php artisan vendor:publish --tag=share-sheet-config
return [ 'app_group' => env('SHARE_SHEET_APP_GROUP'), // iOS; default: "group." + NATIVEPHP_APP_ID 'accept' => [ 'urls' => 1, // most links in one share; 0 to turn off 'text' => true, 'images' => 10, 'files' => 5, ], 'convert_heic' => true, // iPhone/iPad photos (HEIC) arrive as JPEG 'confirmation' => 'Saved to :app', // :app = your app's name];
On iOS your app only appears in the share sheet for content it accepts. Android's share menu lists your app for all of these, and anything you don't accept is quietly dropped.
Checking the setup#
php artisan share-sheet:doctor # your project and your last iOS / Android buildsphp artisan share-sheet:doctor --apple # plus your App IDs and App Groups on Apple's side
Every ✗ line says exactly what to fix. --apple uses the App Store Connect API key NativePHP already uses for uploads (APP_STORE_API_KEY_ID, APP_STORE_API_ISSUER_ID, APP_STORE_API_KEY_PATH). It only reads your account; it never changes it. Apple's API can't show which App Group is ticked on an ID, so check that one by eye.
On the device:
ShareSheet::status();// ['available' => true, 'app_group' => 'group.com.example.myapp', 'container_ok' => true, 'pending' => 0, 'problem' => null]
Testing your app#
use Firsthand\ShareSheet\Facades\ShareSheet; $fake = ShareSheet::fake() ->url('https://example.com/recipe') ->text('Double the butter') ->image(base_path('tests/fixtures/cake.jpg')); // copied, so your fixture is never moved $this->get('/inbox')->assertSee('example.com/recipe'); $fake->assertTaken(3);$fake->assertNothingWaiting();
fake() also lets you build your "something was shared" screens in the browser without a phone.
Good to know#
- The first time, people find your app under "More" in the iOS share sheet. iOS hides new share destinations until they're switched on, so it's worth a line in your onboarding.
- Photos are JPEG by default. iPhone and iPad photos are HEIC, which web views and most servers can't display. They're converted on the way in, keeping orientation and metadata.
- Testing a TestFlight update? If the app still shows your old pages after updating, delete it and reinstall. NativePHP Mobile keeps the previously unpacked app across updates; this isn't the plugin.
- Nothing is sent anywhere. Shared content stays on the device, in your app's own storage, until your code does something with it.
Troubleshooting#
| What you see | What it means |
|---|---|
| App missing from the iOS share sheet | Look under More. Then run share-sheet:doctor: "Extension files ✗" means the build hook didn't run. NativePHP only warns about that and builds anyway, so read the build output for a "Share sheet:" line. |
pending() throws "cannot open the App Group" |
The App Group isn't registered, or isn't ticked on both IDs. See Apple setup. |
| CI signing fails: "No profiles for '….ShareExtension' were found" / "Authentication failed" | Xcode's automatic signing can't create App Groups with an API key. Register the group by hand (step 1), then build again. |
| iOS refuses to install: "does not have a CFBundleDisplayName" | You're on a build from before the plugin was installed. Rebuild. |
| A shared photo shows as a broken image | It's HEIC and convert_heic is off, or you're displaying the temporary path directly. store() it first. |
License#
Commercial. One purchase covers unlimited apps, including client work. The source may not be redistributed. See the LICENSE file in the package.
Made by The Firsthand Co.