BigBrainParking/parksmarter-client/README.md
Hank 1dede50995 Initial commit: parksmarter-client + BigBrainParking app
Reverse-engineered ParkSmarter API client (TypeScript, live-verified) plus a
de-Googled Expo/React Native app for GrapheneOS: MapLibre meter map with GPS,
VisionCamera QR kiosk scanning with save/share, local session-expiry reminders,
UnifiedPush wiring, and Gitea CI to publish signed APKs for Obtainium.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:33:47 -07:00

195 lines
8.6 KiB
Markdown

# parksmarter-client
An unofficial, typed **TypeScript** client for the ParkSmarter (IPS Group) parking
API. It was reverse-engineered from the official Android app
`com.ipsgroupinc.parksmarter` 4.4.0 — a React Native / Hermes build — by decompiling
the JS bundle and recovering every endpoint path, header, app token, and
request/response field name.
TypeScript was chosen because the target is "an alternative Android app or web app":
the same package runs unchanged in a browser, in React Native (Expo/bare), and in
Node 18+. It has **zero runtime dependencies** (uses `fetch`). If you specifically
want a native-Kotlin client instead, the `environments.ts` + endpoint table below map
directly onto Retrofit/OkHttp — ask and it can be ported.
## Install / build
```bash
cd parksmarter-client
npm install
npm run build # emits dist/
```
Import from source (`src/index.ts`) or the built `dist/`.
## Quick start
```ts
import { ParkSmarterClient } from './src';
const ps = new ParkSmarterClient({ environment: 'prodv2' }); // default env
// 1) Bootstrap — seeds a ParkSmarter_SessionId and returns feature flags.
const validity = await ps.getApplicationValidity();
if (validity.Config?.IsInMaintenanceMode) throw new Error('maintenance');
// 2) Log in — Auth_Token + SessionId are stored automatically.
await ps.loginWithPhone({ phoneNumber: '5551234567', password: 'hunter2' });
// 3) Use authenticated endpoints.
const me = await ps.getUserDetail();
const nearby = await ps.getMetersByLocation({ latitude: 40.44, longitude: -79.99 });
// 4) Start a session.
await ps.startParkingSession({
creditCardId: me.CreditCardDetails![0].CCID!,
vehicleId: me.VehicleDetails![0].VehicleID!,
zoneId: nearby.Zones![0].ZoneId!,
spaceId: nearby.Zones![0].Spaces![0].SpaceID!,
customerId: nearby.Zones![0].CustomerID!,
meterTypeId: nearby.Zones![0].MeterTypeId!,
startTime: new Date().toISOString(),
endTime: new Date(Date.now() + 3600_000).toISOString(),
minutesToPurchase: 60,
parkingCost: 2.0,
transactionFee: 0.35,
});
```
### React Native / Node < 18
Pass a `fetchImpl` if the global `fetch` isn't present, and use persistent storage
for tokens:
```ts
import { ParkSmarterClient, TokenStore } from './src';
import * as SecureStore from 'expo-secure-store';
const tokens: TokenStore = {
getAuthToken: () => SecureStore.getItemAsync('ps_auth'),
setAuthToken: (t) =>
t ? SecureStore.setItemAsync('ps_auth', t) : SecureStore.deleteItemAsync('ps_auth'),
getSessionId: () => SecureStore.getItemAsync('ps_session'),
setSessionId: (s) =>
s ? SecureStore.setItemAsync('ps_session', s) : SecureStore.deleteItemAsync('ps_session'),
};
const ps = new ParkSmarterClient({ tokens /*, fetchImpl: fetch */ });
```
## Authentication model
This API does **not** use OAuth/Bearer. Auth is carried in custom headers:
| Header | Meaning | Source |
| --- | --- | --- |
| `Application_Token` | Identifies the app build. Required on **every** request. | Per-environment constant (baked in). |
| `X-Request-Id` | Fresh UUID per request. | Generated by the client. |
| `Auth_Token` | The logged-in user's token. | Returned in the **body** of `POST /api/Auth` as `Auth_Token`; sent as a header on authenticated calls. |
| `ParkSmarter_SessionId` | Server session id. | Seeded by `GET /api/ApplicationValidity` and login; echoed on some responses. |
| `Content-Type: application/json` | On POST/PUT only. | — |
The client persists `Auth_Token` and `SessionId` into the `TokenStore` automatically.
Every request also carries a `localeCode` query param (default `en`).
> Note: a `POST /api/Auth` returning HTTP **201** is treated by the app as a
> "needs verification / not a success" case, not a normal login. Handle 201 distinctly
> if you see it.
## Environments
| Name | Base URL | Notes |
| --- | --- | --- |
| `dev` | `https://dev-parksmarter-api.ipsmeters.com` | |
| `stage` | `https://staging-parksmarter-api.ipsmeters.com` | |
| `test` | `https://testing-parksmarter-api.ipsmeters.com` | |
| `prodv1` | `https://api.parksmarter.com` | |
| `prodv2` | `https://apiv2.parksmarter.com` | **default** |
| `prodv3` | `https://apiv3.parksmarter.com` | |
Application tokens for each are in `src/environments.ts`.
## Endpoint reference
Every method maps to one endpoint. Method is inferred from the app's naming.
| Client method | HTTP | Path |
| --- | --- | --- |
| `getApplicationValidity()` | GET | `/api/ApplicationValidity` |
| `loginWithPhone()` / `loginWithApple()` | POST | `/api/Auth` |
| `logoutAllDevices()` | POST | `/api/Auth/Logout` |
| `signUp()` | POST | `/api/User` |
| `getUserDetail()` | GET | `/api/User` |
| `updateProfile()` | PUT | `/api/User` |
| `requestDeleteUser()` | DELETE | `/api/User` |
| `isEmailRegistered()` / `isPhoneRegistered()` | GET | `/api/User` |
| `requestResetPassword()` | POST | `/api/Password` |
| `updatePassword()` | PUT | `/api/Password` |
| `requestVerifyUser()` | POST | `/api/UserVerification` |
| `verifyUser()` | GET | `/api/UserVerification` |
| `updateDeviceToken()` | PUT | `/api/Device` |
| `addVehicle()` | POST | `/api/Vehicle` |
| `updateVehicle()` | PUT | `/api/Vehicle` |
| `deleteVehicle()` | DELETE | `/api/Vehicle` |
| `addCard()` / `updateCard()` | POST | `/api/Card` |
| `setCardDefault()` | PUT | `/api/Card` |
| `deleteCard()` | DELETE | `/api/Card` |
| `getMetersByLocation()` | GET | `/api/Meter` (`Lat`,`Long`) |
| `getLimitedMetersByLocation()` | GET | `/api/MeterList` (`Lat`,`Long`) |
| `getMetersByZoneName()` / `searchMetersByZoneOrSpace()` | GET | `/api/Meter` (`ZoneName`/`Query`) |
| `getMetersBySerialNumber()` / `getMetersByScannerCode()` | GET | `/api/Meter` (`TerminalSerNo`/`ScannerCode`) |
| `getParkingLots()` | GET | `/api/ParkingLogix` |
| `getParkingEstimateMulti()` | GET | `/api/ParkingEstimateMulti` |
| `getParkingEstimateSingle()` | GET | `/api/ParkingEstimate` |
| `getParkingEstimateItems()` | GET | `/api/ParkingEstimateItems` |
| `startParkingSession()` | POST | `/api/Session` |
| `getActiveParkingSessions()` | GET | `/api/ParkingSession` |
| `getPastParkingSessions()` | GET | `/api/Session` |
| `getParkingReceipt()` | GET | `/api/ParkingReceipt` |
| `emailParkingReceipt()` | POST | `/api/ParkingReceipt` |
| `getNotificationSettings()` | GET | `/api/Setting` |
| `setNotificationSettings()` | POST | `/api/Setting` |
| `getStates()` | GET | `/api/State` |
| `getAbout()` / `getFAQ()` / `getPrivacyPolicy()` / `getTerms()` | GET | `/api/ParkSmarter*` |
## Field-name conventions
The server uses PascalCase ("PSJSON"). This client accepts friendly camelCase inputs
and maps them to the wire format for you; **responses are returned as the raw server
PascalCase JSON** and typed accordingly in `src/types.ts`. Response interfaces include
an index signature because not every optional field is guaranteed on every call — the
well-known fields are typed explicitly.
## Accuracy & verification status
Request shapes were recovered from the app code and are exact. Response models were then
**verified against production** by logging in and calling every read-only endpoint (see
`sweep.mjs`, which records field-names+types only — no PII). Each interface in
`src/types.ts` is annotated CONFIRMED or UNCONFIRMED.
- **CONFIRMED via live capture:** login (`AuthResponse`), `UserDetail`, `VehicleDetail`,
`CreditCardDetail`, `Zone`/`Space`/`SpacePolicy`, `ParkingLot`/`ParkingLotDetail`, all
three estimate responses (`ParkingDetail` price ladder), notification-settings envelope,
states wrapper, the password-reset flow, and the shared `Response` envelope.
- **UNCONFIRMED (no session history on the test account):** `ActiveSession`,
`PastSession`, `ParkingReceipt`. Field names are from static analysis; capture from an
account with at least one past/active session to confirm.
Two behaviors worth knowing (both confirmed live):
1. **Meter search requires auth.** `/api/Meter` and `/api/MeterList` return **401** without a
valid `Auth_Token`, despite being data lookups.
2. **`Response` envelope + token refresh.** Most authenticated responses embed
`Response: { Auth_Token, Message, Status }`. When `Response.Auth_Token` (or a top-level
`Auth_Token`) is non-empty, it's a rolling refresh of your user token — the client stores
it automatically.
To capture the remaining UNCONFIRMED models, re-run `sweep.mjs` on an account that has a
saved card and session history. There's **no TLS pinning**, so a proxy capture on a rooted
device/emulator is also an option if you'd rather see the app's own traffic.
## Legal
For interoperability/research with your own account. Not affiliated with or endorsed by
IPS Group / ParkSmarter. Respect their Terms of Service and applicable law.
```