Force Update Plugin for NativePHP Mobile#
Cross-platform minimum-version gate that opens the correct App Store or Play listing.
Used in: PARqore's own apps New Life Daily, Hard Moments, ColorDrop, and GridUnlock.
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 Native Share:
- Video: NativePHP Mobile: Speech to Text, Text to Speech, Share & Force Update
- Demo app on GitHub
- Hands-on blog post
Overview#
You set a minimum (and optional latest) version in PHP. The plugin reads the running app version from NativePHP — config('nativephp.version'), which is NATIVEPHP_APP_VERSION — and reports whether the installed build is below that minimum. openStore() launches the iOS App Store URL or Android Play listing URL you configured.
This is the product buyers actually need: "this installed build is too old — block or warn, and send them to the store."
It is not a clone of wilsonatb/in-app-update. That plugin wraps Google Play Core flexible/immediate flows on Android only; iOS returns unsupported. This plugin is iOS and Android, PHP-side semver, and a store listing open. It does not show Google's in-app update bottom sheet. Play Core is a different plugin.
mode (hard or soft) is a status flag. hard means the app should treat the build as blocking. The plugin does not hijack navigation — your app reads status() and shows its own screen.
Installation#
composer require partek/force-update
Don't forget to register the plugin:
php artisan native:plugin:register partek/force-update
If you have not published NativePHP's plugin provider yet:
php artisan vendor:publish --tag=nativephp-plugins-provider
Rebuild after native or manifest changes:
php artisan native:run
Usage#
Configure once at boot#
Call configure() once from NativeServiceProvider::boot or AppServiceProvider::boot.
Do not put boot-time configure() (or a set()-style call) in a Livewire component or NativeComponent mount() / render(). Shane rejected that pattern for Home Screen Shortcuts; the same rule applies here. Screens read status. Providers configure.
use PARTek\ForceUpdate\Facades\ForceUpdate; public function boot(): void{ ForceUpdate::configure([ 'minimum' => '1.2.0', 'latest' => '1.4.0', 'ios_url' => 'https://apps.apple.com/app/id123456789', 'android_url' => 'https://play.google.com/store/apps/details?id=com.example.app', 'mode' => 'hard', ]);}
PHP (Super Native / Livewire / Blade)#
use PARTek\ForceUpdate\Facades\ForceUpdate; $status = ForceUpdate::status();// [// 'current' => '1.1.0',// 'minimum' => '1.2.0',// 'latest' => '1.4.0',// 'outdated' => true,// 'storeUrl' => 'https://apps.apple.com/app/id123456789',// 'mode' => 'hard',// ] if (ForceUpdate::isOutdated()) { // Render your own blocking or warning screen. // The plugin will not redirect for you.} ForceUpdate::openStore(); // bool — false if this platform has no URL
outdated is current < minimum. latest is informational only. A DEBUG NATIVEPHP_APP_VERSION (NativePHP's development default) is never outdated.
JavaScript (Vue / React / Inertia)#
configure(), status(), and isOutdated() are PHP-only. Compute them on the server and pass storeUrl into the page. JS wraps the native store open:
import { openStore, ForceUpdate } from '../../vendor/partek/force-update/resources/js/index.js'; await openStore({ url: storeUrl });// orawait ForceUpdate.openStore({ url: storeUrl });
The JS client POSTs to /_native/api/call with an X-CSRF-TOKEN header and throws on HTTP or native errors.
Events#
None.
An UpdateRequired event on every status() call would fire on every request that reads the gate. configure() runs at boot and must not imply "outdated" until the app asks. Skipping events is intentional.
Methods#
configure(array $options): void#
| Key | Type | Required | Description |
|---|---|---|---|
minimum |
string | yes | Semver minimum (e.g. 1.2.0). Installed builds below this are outdated. |
latest |
string | no | Optional newest published version. Informational only. |
ios_url |
string | no | App Store listing URL. Accepted schemes (same list for both URLs): https, http, itms-apps, itms, market. |
android_url |
string | no | Play listing URL (same scheme list as ios_url). |
mode |
hard | soft |
no | Status flag. Default hard. hard means the app should block; the plugin still does not intercept routes. |
Throws if minimum is missing or not semver, if latest / URLs / mode are invalid, or if unknown keys are passed.
status(): array#
| Key | Type | Description |
|---|---|---|
current |
string | Running app version from config('nativephp.version'). |
minimum |
string | Configured minimum. |
latest |
string | null | Configured latest, if any. |
outdated |
bool | true when current < minimum. |
storeUrl |
string | null | Listing URL for this platform, or null if missing / unknown platform. |
mode |
hard | soft |
Configured mode. |
Throws if configure() has not been called.
isOutdated(): bool#
true when the running version is a valid semver and is less than minimum. DEBUG and unreadable versions return false.
openStore(): bool#
Opens the platform store URL via native (UIApplication.open on iOS, Intent.ACTION_VIEW on Android).
Returns false and does not call native when:
- the URL for this platform is missing
- the platform cannot be detected (off-device / tests without a platform)
- the native bridge is unavailable or reports an error
Fail closed. Document the missing URL in your app UI (status()['storeUrl'] is null).
Platform behavior#
| Platform | Version source | Store open |
|---|---|---|
| iOS | config('nativephp.version') |
UIApplication.shared.open |
| Android | config('nativephp.version') |
Intent.ACTION_VIEW |
| Off-device | same config / NATIVEPHP_APP_VERSION |
openStore() returns false |
- Android min SDK 26. iOS min 18.0 in the manifest (the open-URL APIs themselves work on iOS 14+).
- No extra permissions. No Play Core dependency.
- Version is read in PHP. There is no
ForceUpdate.AppVersionbridge.
Testing#
The plugin ships Pest tests for semver compare (1.0.19 vs 1.0.18, 2.0.0 vs 1.9.9), configure validation, missing-URL fail-closed behavior, and the test helpers below.
composer test
There are two ways to test code that uses this plugin. Plain PHP tests can read the gate straight from the facade. The test helpers work with NativePHP's component test harness (Native::test()), which suits screens: the screen calls the real facade, and the helpers set the installed version, answer the bridge the way the device does, and assert on the store open.
Testing with the facade#
App tests can construct status without native:
use PARTek\ForceUpdate\Facades\ForceUpdate; config()->set('nativephp.version', '1.0.18'); ForceUpdate::configure([ 'minimum' => '1.0.19', 'ios_url' => 'https://apps.apple.com/app/id123456789',]); expect(ForceUpdate::isOutdated())->toBeTrue();
openStore() only hits the native bridge when a platform URL exists. Off-device it returns false.
Test helpers#
On NativePHP Mobile 4.0 or later, the plugin registers these helpers on Native::fakeBridge() whenever the app runs under a test runner (APP_ENV=testing). Production boots never register them, and there is nothing extra to install. The plugin fires no events, so every helper is a bridge helper.
use App\NativeComponents\UpdateGate;use Native\Mobile\Testing\Native; it('sends outdated installs to the App Store', function () { Native::fakeBridge() ->withInstalledAppVersion('1.1.0') ->withAppStoreOpening(); Native::test(UpdateGate::class, platform: 'ios') ->assertSee('Update required') ->tap('Update now') ->assertAppStoreOpened('https://apps.apple.com/app/id123456789');});
The minimum, latest and store URLs come from your app's own ForceUpdate::configure() call, which runs at boot in tests too.
Script the bridge first. With nothing scripted, the fake bridge answers null and openStore() returns false. assertAppStoreOpened() would still pass while your screen runs its failure branch.
| Helper | Called on | What it does |
|---|---|---|
withInstalledAppVersion(string $version) |
bridge | Sets config('nativephp.version'), the installed version the gate reads. Call it before Native::test() so the screen's mount() sees it; after that, it applies from the next status() or isOutdated() call. 'DEBUG' behaves like a development build and is never outdated. |
withAppStoreOpening(bool $opens = true) |
bridge | openStore() returns true: the bridge answers {opened: true}, as iOS and Android do. With false, it answers Android's open_failed error and openStore() returns false. |
assertAppStoreOpened(?string $url = null) |
bridge | openStore() reached the bridge, with any URL or with exactly $url. |
assertAppStoreNotOpened() |
bridge | openStore() never reached the bridge. |
The helpers work on Native::fakeBridge() or straight off the harness. Called off the harness, they return the harness, so the chain continues.
- Platform.
openStore()andstatus()['storeUrl']need to know the platform, so pass it to the harness:Native::test(UpdateGate::class, platform: 'android'). Without one, the store URL isnullandopenStore()returnsfalsewithout calling the bridge, soassertAppStoreOpened()fails. OutsideNative::test(), set it with core'sNative\Mobile\Platform::set('ios'). That value lasts for the whole PHP process until the nextNative::test()orPlatform::set(null). ADevice.GetInfoanswer you script on the bridge takes precedence over both. - Missing URLs. If
configure()has no URL for the platform,openStore()returnsfalsewithout calling the bridge, andassertAppStoreNotOpened()passes. - Error codes. Android sends
open_failedwhen no app can open the listing. Both platforms also sendmissing_urlandinvalid_urlfor an empty URL or a scheme they reject, andconfigure()validation catches most of those before the call. iOS answers{opened: true}as soon as it has asked the system to open the URL.openStore()only returns a bool, so the code never reaches your screen. - Mocking the facade. The plugin has no
ForceUpdate::fake(). If you mock the facade withForceUpdate::shouldReceive(), nothing reaches the bridge andassertAppStoreOpened()fails.
License#
Proprietary commercial. See LICENSE.