NativePHP Salon Mobile
#StyleHub — Mobile App
The customer-facing StyleHub app, built with NativePHP Mobile v3, Livewire 4 and Tailwind 4. It talks to the salon-api backend over HTTP; the on-device SQLite database holds only local state.
#Requirements
- PHP 8.3+, Composer, Node 20+
- Android Studio with an emulator (API 33+) — Android is the primary target
- The
salon-apiproject served by Herd nativephp/mobile-secure-storage(paid plugin) — stores the auth token in the device keystore
#Authentication
The customer signs in with an SMS one-time code, or with email and password. Either way the API returns a Sanctum token, and that token is the whole of the client-side auth state — there is no local user table. CustomerAuth fetches the customer from GET /me, and a token the API rejects is discarded on the spot, so a revoked token cannot leave the app half-signed-in.
Where that token lives depends on where the app is running, behind the TokenStore contract:
| Context | Store | Backing |
|---|---|---|
| Device | SecureStorageTokenStore |
iOS Keychain / Android EncryptedSharedPreferences |
| Browser, tests | SessionTokenStore |
The session |
The device store needs the paid plugin:
composer require nativephp/mobile-secure-storage php artisan native:plugin:register nativephp/mobile-secure-storage
A trap worth knowing about. Do not decide "am I on a device?" with
function_exists('nativephp_call'). NativePHP also defines that function on your dev machine, as a PHP fallback that relays calls to a connected device — so it is always true, and the app would reach for a keystore that isn't there. That failure is silent:SecureStorage::get()just returnsnulland nobody can sign in. The reliable test is the function's origin — on a device it comes from NativePHP's C extension (ReflectionFunction::isInternal()), everywhere else it is userland PHP. SeeAppServiceProvider::runningOnDevice().
#Reading OTP codes in development
There is no SMS gateway locally. The API's log SMS driver writes the code to salon-api's log (php artisan pail), and because OTP_EXPOSE_CODE defaults to the API's debug flag, the code also comes back in the API response — the OTP screen shows it inline. Never enable that in production: it would hand any caller a login for any phone number.
#Setup
composer install npm install cp .env.example .env php artisan key:generate php artisan native:install
Then build the assets and launch the app (see "Running" below).
#Talking to the API from the emulator
This is the one piece of setup that is not obvious.
The API is served by Herd at http://salon-api.test. Two things make that URL unusable from an Android emulator:
- The emulator is a separate virtual device, so
localhostmeans the emulator itself, not your Mac. The host machine is reachable at the special address10.0.2.2. - Herd's
.testdomains are resolved by a local DNS resolver on your Mac. The emulator does not use it, sosalon-api.testdoes not resolve on the device.
Pointing at http://10.0.2.2 alone is not enough either: Herd serves many sites from one port and picks between them using the Host header, so a bare IP request lands on the wrong site.
The approach used here: address the host machine by IP and send the site name as a Host header, which is exactly what Herd's nginx routes on. ApiClient does this whenever SALON_API_HOST is set:
SALON_API_URL=http://10.0.2.2 # the host machine, as seen from the emulator SALON_API_HOST=salon-api.test # the Herd site to route to
To run the app in a desktop browser instead (handy for quick UI work), point it straight at the site and drop the header:
SALON_API_URL=http://salon-api.test SALON_API_HOST=
Alternatives considered: herd share gives a public HTTPS URL that works from any device without a Host header, but the URL changes each session. The Host-header approach needs no tunnel and no extra account, so it is the default.
Note that Herd serves this site over http, not https — which conveniently means there is no TLS certificate for the emulator to distrust.
#Running
Always pass the platform when building assets:
npm run build -- --mode=android
php artisan native:run android
Or build, deploy and start hot reload in one step:
php artisan native:run android --watch
#Design system
The StyleHub look — near-black green canvas, lime accent, pill controls, rounded cards — is defined once as Tailwind theme tokens in resources/css/app.css (bg-canvas, text-content, bg-accent, .card, .btn-accent, .chip). Prefer those tokens over raw hex values.
Navigation chrome is native: the bottom tab bar is an EDGE <native:bottom-nav> in resources/views/layouts/app.blade.php, rendered outside the web view by the native shell. Its item URLs must stay relative — an absolute URL is treated as external and opens the device browser instead of navigating the app.
#Tests
php artisan test # Pest vendor/bin/pint --dirty # formatting vendor/bin/phpstan analyse