Home Screen Shortcuts Plugin for NativePHP Mobile#
Home Screen long-press shortcuts for NativePHP Mobile: register iOS UIApplicationShortcutItem / Android App Shortcuts that deep-link into a named route.
Used in: PARqore's own apps New Life Daily, FarmSeconds, Hard Moments, ColorDrop, and GridUnlock.
Overview#
Long-press the launcher icon and most apps show 2–4 shortcuts. This plugin is one facade: pass up to four { id, title, route|path } items and it registers them with the OS.
Without the plugin, the same feature requires app-specific ShortcutManagerCompat Kotlin and UIApplicationShortcutItem Swift. Your application supplies shortcut definitions; the plugin owns the platform registration.
A tap is a normal deep link through NATIVEPHP_DEEPLINK_SCHEME. NativePHP's own router opens the screen. The plugin does not dispatch a tap event.
How a tap gets into your app:
- Android (warm and cold): the shortcut launches
HomeScreenShortcutsTrampolineActivity, which forwardsACTION_VIEWto the launcher activity withCLEAR_TOP | SINGLE_TOP. That avoidsNEW_TASK | CLEAR_TASKtearing down a runningMainActivity(which can let a stale route win). - iOS, warm resume:
application(_:performActionFor:completionHandler:)forwards the stored URL intoDeepLinkRouter. - iOS, cold start (force-quit): not covered. iOS delivers
.shortcutItemtodidFinishLaunchingWithOptions. NativePHP'sAppDelegatealready implements that method and only checks.url/.userActivityDictionary. A Swift extension cannot override it. Closing the gap needs an upstream change innativephp/mobile.
Installation#
composer require partek/home-screen-shortcuts
Don't forget to register the plugin:
php artisan native:plugin:register partek/home-screen-shortcuts
Set NATIVEPHP_DEEPLINK_SCHEME in .env. Shortcuts will not open a screen without it, and HomeScreenShortcuts::set() throws if the scheme is missing.
Rebuild after you add or change shortcuts (php artisan native:run android / ios). Composer install alone does not update the launcher.
Usage#
PHP (Super Native / Livewire / Blade)#
The facade is for Super Native first — and the same call works from Livewire or Blade.
Where to call this: call HomeScreenShortcuts::set() once at app boot in a service provider (NativeServiceProvider::boot or AppServiceProvider::boot). Call clear() on logout from wherever you handle logout. Do not put set() inside a one-off screen component.
// app/Providers/NativeServiceProvider.php (or AppServiceProvider) use PARTek\HomeScreenShortcuts\Facades\HomeScreenShortcuts; public function boot(): void{ HomeScreenShortcuts::set([ ['id' => 'today', 'title' => "Today's Walk", 'route' => 'walk.today'], ['id' => 'talk', 'title' => 'Ask Pauly', 'subtitle' => "Open Let's Talk", 'route' => 'talk'], ['id' => 'new-post', 'title' => 'New Post', 'path' => '/posts/create'], ]);}
On logout (wherever you already sign the user out):
HomeScreenShortcuts::clear();
Each action needs id and title, plus exactly one of:
route— named route, optionalparams, resolved withroute($name, $params, absolute: false)path— raw relative path, used as-is
subtitle is optional (iOS shows it; Android may ignore it). More than four actions: extras are dropped.
JavaScript (Vue/React/Inertia)#
import { set, clear } from '../../vendor/partek/home-screen-shortcuts/resources/js'; // JS talks to the same HomeScreenShortcuts.Set / Clear bridges.// PHP is what resolves route/path into a NATIVEPHP_DEEPLINK_SCHEME URL;// from JS pass that already-built url on each action.await set([ { id: 'today', title: "Today's Walk", url: 'myapp://walk/today' }, { id: 'talk', title: 'Ask Pauly', subtitle: "Open Let's Talk", url: 'myapp://talk' }, { id: 'new-post', title: 'New Post', url: 'myapp://posts/create' },]); await clear();
Lowercase set / clear are the primary JavaScript API. Set / Clear aliases are also exported for existing code that mirrors the native bridge names. set and Set accept either an actions array or { actions: [...] }.
Events#
This plugin dispatches no events. There is no ShortcutTapped (or any other plugin event) to listen for.
A shortcut tap is a deep link. The native side stores NATIVEPHP_DEEPLINK_SCHEME://… on the shortcut; NativePHP's own router opens that path — the same mechanism as any other link on your scheme. Handle the destination as a normal Laravel route.
Methods#
set(array $actions)#
Replace all dynamic shortcuts. Returns bool.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Stable id for the OS |
title |
string | yes | Launcher label |
route |
string | one of route/path | Named Laravel route |
params |
array | no | Route parameters |
path |
string | one of route/path | Relative path |
subtitle |
string | no | iOS subtitle |
The PHP facade resolves route/path into { id, title, subtitle, url } before calling HomeScreenShortcuts.Set. Extra actions beyond 4 are dropped.
clear()#
Remove all dynamic shortcuts. Returns bool. Calls HomeScreenShortcuts.Clear.
Platform Behavior#
Android#
ShortcutManagerCompat; no-op below API 25- Default icon = app launcher icon
- Min SDK 24
- No extra permissions
- Trampoline activity is the warm and cold tap path (see Overview)
iOS#
UIApplicationShortcutItem- Min iOS 14.0
- No extra permissions
- Warm resume works via the AppDelegate extension; cold start / force-quit does not (see Overview)
Testing#
Test helpers#
On a NativePHP core with the macroable test harness (nativephp/mobile 4.0.0 or later), this plugin adds Home Screen shortcut helpers to Native::fakeBridge() and Native::test(), so your app's tests can check the shortcuts a screen registers and launch the app from one, without a device:
use App\NativeComponents\Settings;use Native\Mobile\Testing\Native; Native::fakeBridge()->withHomeScreenShortcuts(); Native::test(Settings::class) ->tap('Add shortcuts') ->assertHomeScreenShortcutsSet(['today', 'talk', 'new-post']) ->assertHomeScreenShortcut('today', "Today's Walk", 'myapp://walk/today') ->launchFromHomeScreenShortcut('today') ->assertNavigatedTo('/walk/today');
| Helper | Registered on | What it does |
|---|---|---|
withHomeScreenShortcuts(bool $supported = true) |
FakeBridge |
Answers HomeScreenShortcuts.Set and .Clear as the device does ({success: true}), so set() and clear() return true. Pass false to have both return false, as set() does when Android rate-limits shortcut updates. |
assertHomeScreenShortcutsSet(?array $ids = null) |
FakeBridge |
Asserts set() reached the bridge. With $ids, asserts the last set() call registered exactly these ids, in order, after the facade drops everything past the fourth. |
assertHomeScreenShortcut(string $id, ?string $title = null, ?string $url = null) |
FakeBridge |
Asserts the last set() call registered shortcut $id, and, when given, its title and deep link (the scheme://path URL the facade builds from route or path). |
assertHomeScreenShortcutsCleared() |
FakeBridge |
Asserts clear() reached the bridge. |
launchFromHomeScreenShortcut(string $id) |
TestableComponent |
Taps shortcut $id while the app is running: delivers the deep link the native shell sends for it, which the screen turns into a navigation. Chain ->assertNavigatedTo('/path'), or ->follow() when the path is a Route::native screen. |
FakeBridge helpers also chain off the harness: Native::test(...)->assertHomeScreenShortcutsSet() returns the harness.
-
Call
withHomeScreenShortcuts()before the screen callsset()orclear(). Without it the fake bridge answersnulland both returnfalse, so the screen runs its failure branch whileassertHomeScreenShortcutsSet()still passes. -
launchFromHomeScreenShortcut()takes the shortcut's URL from the lastset()call and routes it the way iOS'sDeepLinkRouterdoes:myapp://walk/today?day=1opens/walk/today?day=1, andmyapp://opens/. It fails with a clear message when noset()call registered that id, or whenclear()ran after the lastset(). It covers a tap while the app is running; the iOS cold-start gap (see Overview) is not simulated. -
Shortcuts set in a service provider's
boot()(as Usage recommends) are set while the test app boots, before your test can bind a fake bridge. The helpers never see that call, and it goes through core's Jump bridge instead (to the connected device whenphp artisan native:jumpis running). Skip it under unit tests and register the same list from the test:Copied!// app/Providers/NativeServiceProvider.phpif (! $this->app->runningUnitTests()) {HomeScreenShortcuts::set(AppShortcuts::all());}// tests/Feature/HomeScreenShortcutsTest.phpNative::fakeBridge()->withHomeScreenShortcuts();HomeScreenShortcuts::set(AppShortcuts::all());Native::test(Home::class)->launchFromHomeScreenShortcut('today')->assertNavigatedTo('/walk/today');AppShortcuts::all()stands for wherever your app keeps its shortcut list. -
If you mock the facade (
HomeScreenShortcuts::shouldReceive(...)), calls never reach the bridge, so the helpers see nothing andlaunchFromHomeScreenShortcut()has no shortcut to launch. -
The helpers are registered only while the app runs unit tests (
APP_ENV=testing). Production boots and v3 cores are untouched.
This package's suite#
PHPUnit covers the PHP contract: resolve() validation, deep-link URL building, the four-action cap, and set()/clear() failing gracefully outside a built app. HomeScreenShortcutsMacrosTest boots the NativePHP core with Testbench and drives the test helpers through the real facade and a small fixture screen. The Node test covers resources/js/index.js. No hardware runs in this repository.
composer installvendor/bin/phpunitnode --test tests/js/shortcuts.test.mjs
Notes#
- Requires
NATIVEPHP_DEEPLINK_SCHEME. - Changed shortcut lists need a native rebuild.
- This is not
nativephp/mobile-shareand not a notification plugin. - No additional Android or iOS permissions.
License#
Proprietary commercial software. See LICENSE.