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>
This commit is contained in:
commit
1dede50995
39 changed files with 3671 additions and 0 deletions
195
parksmarter-client/README.md
Normal file
195
parksmarter-client/README.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# 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.
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue