Health Plugin for NativePHP Mobile#
Read steps, activity, heart rate, weight, sleep and workouts from Apple Health and Health Connect, and save weight, height and water, from your NativePHP app.
See it in action#
- iPad: allow access with Apple's sheet, log a glass of water, read it back
- Android: Health Connect permission, steps and activity, log a glass of water
Overview#
- One API, both platforms. HealthKit on iOS, Health Connect on Android, with the same method names and the same units everywhere: steps, metres, kcal, litres, bpm, kg.
- Shown in each person's own units.
Health::preferredUnit('weight')sayslbfor someone in the US (or whatever they picked in the Health app), andHealthUnitsconverts: pounds, fluid ounces, miles, inches. - Totals that don't double count. If a phone and a watch both record your walk,
Health::today('steps')counts it once, the same way the Health apps do. - Answers straight away.
Health::latest('weight')returns the reading. You don't wait for an event. - The fiddly setup is done for you. On iOS, the build adds the HealthKit entitlement and your permission wording. On Android it adds only the Health Connect permissions you asked for, plus the privacy-policy screen Google requires. Config mistakes stop the build with a plain message.
Requirements: NativePHP Mobile 4.x, Laravel 11–13, PHP 8.3+, iOS 18+ (iPhone, and iPad with iPadOS 17+), Android 10+ with Health Connect (built in from Android 14; an app from Google Play before that).
Installation#
composer require firsthand/nativephp-health
Don't forget to register the plugin, and publish the config:
php artisan native:plugin:register firsthand/nativephp-healthphp artisan vendor:publish --tag=health-config
In config/health.php, list what your app reads and writes. Ask only for what you use: Apple reviews your reasons, and Google Play asks you to justify each Health Connect permission.
'read' => ['steps', 'active_energy', 'weight', 'sleep'],'write' => ['water'],
Then set the wording people see, and your privacy policy:
HEALTH_READ_REASON="Shows your daily steps and sleep next to your goals."HEALTH_WRITE_REASON="Saves the water you log to Apple Health."HEALTH_PRIVACY_POLICY_URL=https://example.com/privacy
Rebuild your app.
Apple: turn on the HealthKit capability for your app ID at developer.apple.com → Identifiers.
Google Play: health apps fill in the Health Connect declaration in Play Console (App content → Health apps).
Usage#
PHP (Super Native / Livewire / Blade)#
use Firsthand\Health\Facades\Health; Health::requestPermission(); // once; the answer arrives as HealthPermissionResult Health::today('steps'); // 8421.0Health::total('distance', now()->subWeek()); // metresHealth::latest('weight'); // ['value' => 71.4, 'unit' => 'kg', 'start' => Carbon, …]Health::samples('heart_rate', now()->subHour()); // newest firstHealth::sleep(now()->subDay()->setTime(18, 0)); // periods with a stage: light, deep, rem, awake…Health::workouts(now()->subMonth()); // type, minutes, energy, distance Health::write('water', 0.25); // litres
Show each person their own units:
use Firsthand\Health\Support\HealthUnits; $unit = Health::preferredUnit('weight'); // 'lb' in the US$shown = HealthUnits::fromBase('weight', $kg, $unit); // 157.4Health::write('water', HealthUnits::toBase('water', 8, 'fl_oz')); // an 8 fl oz glass
JavaScript (Vue / React / Inertia)#
import { requestPermission, today, latest, write, onHealthPermissionResult } from '@firsthand/nativephp-health'; onHealthPermissionResult(({ denied }) => { if (denied.length) showWhy(); });await requestPermission(); const steps = await today('steps');await write('water', 0.25);
Dates go in as Date objects or ISO strings. TypeScript types are included.
Events#
HealthPermissionResult#
Firsthand\Health\Events\HealthPermissionResult, fired when the permission sheet closes.
| Property | Type | Description |
|---|---|---|
granted |
list<string> |
Permissions allowed, such as read:steps or write:water |
denied |
list<string> |
Permissions turned off |
unknown |
list<string> |
iOS read permissions: Apple never tells an app whether reading was allowed |
ok |
bool |
false if the sheet couldn't be shown |
message |
?string |
Why not |
use Firsthand\Health\Events\HealthPermissionResult;use Native\Mobile\Attributes\On; #[On(HealthPermissionResult::class)] // #[OnNative(...)] in a Livewire web-view pagepublic function healthAnswered(array $granted, array $denied): void { … }
Methods#
| Method | Returns |
|---|---|
status() |
['platform', 'available', 'reason']. reason is unsupported, not_installed or update_required (Android) |
requestPermission() |
bool (the answer arrives as HealthPermissionResult) |
total($type, $from, $to = now) |
?float total of steps, distance, active_energy or water |
today($type) |
?float total since midnight |
samples($type, $from, $to = now, $limit = 100) |
readings, newest first: value, unit, start, end, source |
latest($type) |
the most recent reading, or null |
sleep($from, $to = now) |
periods: stage, start, end, minutes, source |
workouts($from, $to = now) |
type, start, end, minutes, energy (kcal), distance (m), source |
write($type, $value, $at = now) |
bool; weight (kg), height (m) or water (L) |
preferredUnit($type) / preferredUnits($types) |
the unit this person reads weight, height, distance, water or active_energy in: their Health app choice on iOS, their region on Android (US: lb, in, mi, fl_oz) |
regionUnits() |
the phone's region's units (US: lb, in, mi, fl_oz; elsewhere metric), ignoring the Health app's own setting. Apple can choose mL for water even in the US |
openSettings() |
opens the Health app (iOS) or Health Connect (Android) |
| Type | Unit | Read | Write |
|---|---|---|---|
steps |
count | ✓ | |
distance |
m | ✓ | |
active_energy |
kcal | ✓ | |
water |
L | ✓ | ✓ |
heart_rate, resting_heart_rate |
bpm | ✓ | |
weight |
kg | ✓ | ✓ |
height |
m | ✓ | ✓ |
sleep |
periods | ✓ | |
workouts |
sessions | ✓ |
A type that isn't in config/health.php, a backwards range or a zero reading throws InvalidArgumentException before it reaches the phone. When the phone refuses, you get Firsthand\Health\Exceptions\HealthException; $e->needsPermission() tells you it's a permission problem. Outside a NativePHP app, calls return null, false or [], so pages still render.
Platform behavior#
- iOS hides read refusals. Apple never tells an app whether someone declined to share a type, so that a "no" looks the same as having no data. A declined read returns
0or an empty list, not an error. Write permissions are reported. - Android needs Health Connect. It is built into Android 14 and later. On Android 10–13 it is an app from Google Play:
status()['reason']isnot_installedorupdate_required, andopenSettings()takes people to it. - Android checks every read. If someone turns a type off in Health Connect, reading it throws a
HealthExceptionwithneedsPermission(). - Health Connect's "privacy policy" link opens your
HEALTH_PRIVACY_POLICY_URLin the browser. - Sleep stages depend on what recorded them. A phone that only knows "asleep" gives you
asleep; a watch giveslight,deepandrem.
Testing#
Script the bridge with NativePHP's testing suite:
use Native\Mobile\Testing\Native; it('shows today’s steps', function () { Native::fakeBridge() ->respondTo('Health.Status', ['platform' => 'ios', 'available' => true]) ->respondTo('Health.Total', ['value' => 8421]); Native::test(ActivityScreen::class)->assertSee('8,421 steps');});
Troubleshooting#
| What you see | Why |
|---|---|
| "Add 'x' to health.read in config/health.php" | Every type you read or write must be listed, and the app rebuilt |
| iOS: everything reads 0 | The person declined, or has no data. Apple doesn't say which (see Platform behavior) |
| iOS: the app crashes when asking | The HealthKit capability is missing from your app ID, or a reason is empty |
Android: not_installed |
Health Connect isn't on this phone (Android 10–13); call Health::openSettings() |
| Android: the build warns about a privacy policy | Set HEALTH_PRIVACY_POLICY_URL; Google Play requires one |
| A native screen shows no text | Native screens need NativePHP's UI package, nativephp/mobile-ui |
License#
One purchase covers unlimited apps. See the LICENSE file in the package.