Widgets

Your app, on the home screen.

Android
$ composer require all1web/nativephp-widgets
Widgets — Your app, on the home screen.

Home-screen widgets driven from PHP: push a payload, the native widget renders it — no fetching, no background PHP.

  • Push a payload from Laravel — the native widget repaints itself
  • In-app “Add widget” prompt via requestPin(), gated on status()
  • First-class testing with Widgets::fake() and assertUpdated()

Documentation

Synced from GitHub 0 seconds ago

📱 nativephp-widgets

Put your app on their home screen.

Your app, on the home screen — an Android widget plus iOS quick actions and a live badge, one push from Laravel

The home screen is the most valuable pixel real estate on a phone: your app's content, visible every time the user looks at their device — no tap, no launch, no notification fatigue. This plugin claims that space on both platforms, straight from Laravel: a real Android home-screen widget with one-tap action and capture buttons, and on iOS the app icon itself goes to work — long-press quick actions and a live badge count, pushed by the same PHP call. You never write a line of native code — the plugin handles the Kotlin and Swift side for you.

Widgets::update() → native store → widget repaints → tap opens your deep link

Why this needs a plugin at all: widgets are an OS-level capability reserved for native apps. There is no web API, no WebView trick, and no PWA path to the Android home screen — a widget is native UI rendered by the launcher itself, even while your app isn't running. This plugin is that capability, delivered as three lines of PHP. It's the only home-screen widget plugin in the NativePHP ecosystem.


✨ What you get

  • 🏠 A real Android home-screen widget — title, up to three content lines, an accent badge, and an "updated at" caption, in a launcher-native card that follows light/dark theme automatically.
  • A quick-actions widget too: a compact row of up to four icon+label buttons (Note · Photo · Voice…), each deep-linking into its own screen — your app's fastest entry points, one tap from the home screen, configured entirely from PHP.
  • 📸 Capture buttons (Android): a button can fire the system camera, sound recorder, or document picker before your app is even open — the result is staged safely and lands in your PHP with three tiny calls, or flows straight into the share-target inbox when that plugin is installed. See Handling captures.
  • 🍏 iOS in the box (beta): the same actions() push publishes the app icon's long-press quick-actions menu (SF Symbol icons, up to 4 entries), an opt-in setting mirrors your badge onto the app icon's badge count — a live number on the iOS home screen, and it never prompts the user unless you explicitly ask it to — and every payload stores App-Group-ready so WidgetKit rendering lands without changing your PHP. See iOS for the honest status.
  • 🐘 Updates from anywhere in Laravel. Controllers, observers, queued jobs — one facade call stores the content natively and repaints every placed widget instantly.
  • 🔗 Tap-through deep links. Send the user to the exact screen the widget is showing, not just your app's front door.
  • 📌 An in-app "Add widget" prompt (Widgets::requestPin()), plus Widgets::status() so you can show the prompt only to users who haven't placed one yet.
  • 🔒 Never blank, never broken. Content survives reboots and launcher restarts, renders offline, and shows a tidy empty state before the first push. The widget never runs PHP and never fetches — it can't jank or drain battery.
  • ♻️ Placed widgets survive updates. The provider identity is a frozen, test-enforced contract — future versions (and the eventual iOS/multi-widget evolution) take over without users' widgets vanishing.
  • 🧪 First-class testing: Widgets::fake() with assertUpdated() / assertCleared() / payload matchers, so your test suite never needs a device.
  • 🩺 php artisan widgets:doctor — diagnoses your whole setup (registration, native build, bridge, content, config) and tells you the exact command that fixes whatever's wrong.
  • 🤖 AI-ready out of the box — Laravel Boost guidelines ship with the plugin, so your AI pair-programmer already knows the whole API, its limits, and the idioms that keep you out of trouble.

🚀 On the roadmap (planned)

  • 🧩 Multiple info-widget kinds: each configured id becomes its own widget in the launcher picker — "Today" and "Stats" side by side, sized differently.
  • 🖼️ Images in widgets: photo thumbnails and icons in the card, fed from app storage the same push-model way.
  • 📋 List template: a scrollable list widget (tasks, headlines, orders) with per-row deep links.
  • 🎨 Design tokens: a Tailwind-inspired vocabulary (accent colors, text emphasis, per-widget theming) mapped onto the native widget renderer — brand the widget surface itself, not just the screens behind it.
  • 🍎 iOS WidgetKit rendering the moment NativePHP platform support for app-extension targets lands — the payload store shipping today is already the one a WidgetKit timeline reads.

📦 Install

After purchasing, connect Composer to the NativePHP plugin marketplace (your credentials are on your Purchased Plugins dashboard), then:

composer config repositories.nativephp-plugins composer https://plugins.nativephp.com
composer config http-basic.plugins.nativephp.com your-email@example.com your-license-key
composer require all1web/nativephp-widgets

Register the plugin (NativePHP plugins are opt-in for security) and rebuild:

php artisan native:plugin:register all1web/nativephp-widgets
php artisan native:install android --force
php artisan native:run android

The rebuild matters: the widget is registered with Android when the app is installed, so it appears in the launcher's widget picker only after this rebuild — not just a re-run of an old build.

# Early access via GitHub (licensees with repo access):
composer config repositories.widgets vcs https://github.com/all1web/nativephp-widgets
composer require "all1web/nativephp-widgets:dev-main"

# Local checkout (plugin development):
composer config repositories.widgets path ../nativephp-widgets
composer require "all1web/nativephp-widgets:*@dev"

🧑‍💻 Use it

Push content whenever your data changes:

use All1web\Widgets\Facades\Widgets;

Widgets::update('default', [
    'title' => 'Today',
    'lines' => ['Inbox: 3', 'Due: 2'],
    'badge' => '5',
    'deeplink' => 'myapp://today',
]);

That's genuinely the whole integration. Add the widget to your home screen (or call Widgets::requestPin() from an "Add widget" button) and it shows exactly what you pushed — instantly, and again after every reboot. If anything looks off, php artisan widgets:doctor walks the whole chain and prints the fix.

The quick-actions row is one more call — a capture combo, for example:

Widgets::actions([
    ['icon' => 'note',  'label' => 'Note',  'deeplink' => 'myapp://capture/note'],
    ['icon' => 'photo', 'label' => 'Photo', 'deeplink' => 'myapp://capture/photo'],
    ['icon' => 'voice', 'label' => 'Voice', 'deeplink' => 'myapp://capture/voice'],
]);

Each button opens your app straight into that screen. Icons ship with the plugin (note, photo, voice, file, plus, star, search), tinted to the launcher theme automatically.

How fresh is it? The widget always shows whatever your app most recently pushed — updates repaint in the same moment update() runs. Pushes happen whenever your PHP runs (screens loading, jobs, any user action), so wire update() into the code paths that change the data the widget shows. It's a snapshot the OS keeps on screen, not a live connection — honest by design, and battery-friendly for it.

And in your test suite:

$fake = Widgets::fake();

// ...code under test that pushes widget content...

$fake->assertUpdated('default', fn ($payload) => $payload->badge === '5');

From JavaScript

Driving a Livewire, Inertia, or SPA front-end? The whole API has a JavaScript twin — same names, same payload shape:

import { widgets } from '@all1web/nativephp-widgets';

await widgets.update('default', {
    title: 'Today',
    lines: ['Inbox: 3', 'Due: 2'],
    badge: '5',
    deeplink: 'myapp://today',
});

await widgets.actions([
    { icon: 'note',  label: 'Note',  deeplink: 'myapp://capture/note' },
    { icon: 'photo', label: 'Photo', deeplink: 'myapp://capture/photo' },
]);

One asymmetry to know about: config/widgets.php is read by PHP, not by JavaScript. The only payload key this affects today is the iOS badge — PHP adds it for you from the ios.badge setting, while a JS caller opts in per call by putting iosBadge: 'mirror' (the never-prompting mode) or iosBadge: 'request' in the update() payload. Same two modes, same behavior described under iOS; everything else is identical on both sides.

One-time setup: npm install ./vendor/all1web/nativephp-widgets links the package into your bundler (Vite picks it up as-is). Every bridge call — update, actions, clear, clearActions, requestPin, status, and the three capture calls — is exported; the reference has the full list and setup notes.

Everything else — clearing back to the empty state, multiple widget ids, deep-link rules, the pin prompt, status() — is covered in the reference:

  • Reference — the full API, payload shape, testing fake, and configuration.
  • Design notes — the architecture and the reasoning behind it.

🤝 Works with Share Target

The capture buttons are built to pair with all1web/nativephp-share-target — but never to require it:

Widgets alone Widgets + Share Target
Photo / Voice / File buttons Staged locally; your app drains with three calls (guide) Delivered into the share inbox — widget captures and real OS shares arrive through one pipeline, tagged with a widget-quick-* subject
Note button Opens your app on a configurable route, ready to type Same (inbox delivery available via the share-target handoff mode)
Content shared from other apps The share sheet lists your app; everything lands in the same inbox

Detection is automatic at tap time and every path falls back gracefully — installing or removing either plugin never breaks the other. Captures are Android-only; on iOS the same buttons appear in the app icon's quick-actions menu and open the app (never claim iOS capture in your copy).


🍏 iOS

One push, two platforms. iOS won't allow a home-screen widget without an app extension — so instead of shipping nothing, this plugin puts the same pushes to work on the surface iOS does open up: the app icon.

Quick actions (beta) — the same Widgets::actions() call publishes the app icon's long-press quick-actions menu: up to four icon+label entries (SF Symbol icons matched to the plugin's icon set), the OS-native one-tap affordance every iPhone user already knows. No extension target, no permissions, no extra code.

Live badge (beta) — set 'ios' => ['badge' => true] in the config and every Widgets::update() also mirrors the payload's numeric badge onto the app icon's badge count: a glanceable, always-current number on the iOS home screen, updated by the exact call that repaints the Android widget. Non-numeric badges ('NEW') resolve to 0, clear() resets the count, and switching the setting back off resets it too — opting out never strands a stale number on the icon.

The default mode never prompts. iOS shows an icon badge only for apps the user has already authorized for notifications, and it offers no badge-only permission dialog to ask with. So true mirrors the count when your app already holds that authorization — because you asked for it, in your own context — and quietly does nothing when it doesn't. Switching the badge on therefore costs your users no dialog at all.

If your app has no notification prompt of its own and you'd rather the plugin do the asking, set 'ios' => ['badge' => 'request']. Choose it deliberately: the first mirrored update presents iOS's standard alert — “Your App” Would Like to Send You Notifications ("Notifications may include alerts, sounds, and icon badges") — and that is your app's single, permanent notification-authorization decision. It covers notifications generally (your app then appears under Settings → Notifications), it is asked exactly once for the life of the install, and a denial rules out every notification feature you might add later. Either way a denial is silent: no badge appears and update() still reports success — iOS gives the plugin no way to surface it.

Home-screen widgets are Android-first, honestly. On iOS they require a WidgetKit app extension — a separate build target the NativePHP platform cannot generate yet. Rather than fake it, the plugin ships the half it can: every push stores its payload on iOS too, App-Group-ready, and the same PHP API is the one iOS home-screen rendering will use. When platform support arrives and this plugin adds its widget extension, you rebuild — your code doesn't change. Until then, be accurate in your app copy: the home-screen widget is an Android feature; the quick actions and badge are cross-platform.

Why beta? Both iOS surfaces are API-stable and covered by the test suite; the label comes off when physical-device validation completes.


🧹 Uninstall

Leaving is as clean as arriving — the plugin never edits your project files, so removal is two commands:

php artisan native:plugin:uninstall all1web/nativephp-widgets
php artisan native:install android --force

The rebuild regenerates the native project from NativePHP's own template, so no widget code, manifest entries, or resources linger anywhere, and the plugin's small on-device store lives inside the app sandbox — it goes wherever the app goes.

Migrating instead of removing? Widgets your users placed are bound to the provider's component name, which this plugin keeps frozen — so a successor implementation (including future first-party platform support) can take over without a single placed widget disappearing. The recipe, plus the one optional file you may have published, is in the reference: Uninstall / revert.


📋 Requirements

  • NativePHP Mobile ^3.3 || ^4.0
  • Android 8.0+ (minSdk 26) · iOS 18.2+
  • Declares zero permissions, and shows the user no dialog of any kind in the default configuration. Two opt-in edges, disclosed honestly: the photo capture button asks for CAMERA at runtime only in apps whose own manifest already declares it, and the iOS badge's 'request' mode — never the plain true setting, which cannot prompt — presents iOS's standard notification-authorization alert. Details in Store review.

🔬 Digging deeper

Doc What's in it
Reference Full API, payload fields, config, deep links, pinning, testing
Handling captures Capture buttons end to end: routes, claiming, tiers, warm events
Store review & cloud builds Exactly what Apple/Google review sees, every opt-in that can raise a dialog, cloud-build readiness
Platform notes Android widget platform realities, so you design with them
Design notes Architecture and the reasoning behind it
Changelog Version history

🛠️ Development

composer install
composer test

🔍 Under the hood

The fine print — everything below is why the three lines of PHP above just work.

⚙️ Engineering you don't have to think about: a strict push → store → render architecture means the widget renders from persisted state in native code only — it cannot block the launcher, fetch over the network, or wake your PHP. The renderer is exception-fenced (a bad payload can never crash the host process), deep links are resolve-checked with a safe launch-intent fallback, and the provider identity is a frozen contract so placed widgets survive every update — enforced by a test that fails loudly on rename.

🧪 Verified, not vibes: the info and quick-actions widget pipeline is verified on physical hardware (Samsung Galaxy Z Fold6, Android 16, One UI launcher): in-app pin prompt → placement, Widgets::update() → payload rendered on the home screen, widget tap → deep-link routed into the app, clear() → instant empty-state repaint. Capture is verified on the same hardware too: requestPin() → placement, capture button → the system camera opening before the app was running, a 4 MB photo staged into app-private storage, the pending route drained on the next app open, claimCapture() returning the item (path, name, mime type, size, kind), and the staging index emptied so a second claim can only return null. That run exercised the photo path; note, voice and file ride the same dispatch, staging and claim code. The iOS surfaces carry a beta label until their own device pass completes. Behind all of it, a growing Pest suite (550+ assertions) pins the manifest/compiler contract, renderer↔layout resource consistency (every id the Kotlin looks up must exist in the shipped layout), payload normalization round-trips, the testing-fake semantics, and the embed-isolation rules that let ALL 1 plugins co-install without conflicts — and every compiler mechanism the manifest relies on was verified against the NativePHP compiler source. php artisan widgets:doctor re-verifies your own setup any time.

🏪 Review-proof by design: zero permissions declared, zero prompts in the default configuration, zero usage-description strings, no private APIs, no extra build targets, no signing changes — invisible to Apple/Google review and safe in any cloud build pipeline. The two opt-in edges (the conditional camera request, and the iOS badge's 'request' mode — the single setting in the plugin that can produce a system dialog, and even badge mirroring's default mode never does) are documented for reviewers in docs/STORE-REVIEW.md. Your AI pair-programmer gets first-class knowledge too: Laravel Boost guidelines ship in the box.

📜 License

Commercial. Distributed as a paid plugin via the NativePHP Plugin Marketplace; each purchase grants a license key used for Composer authentication. Licensed by ALL 1, a Wyoming corporation. Source access is included for your own development; redistribution of source is not — see LICENSE for the full EULA.