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
713
parksmarter-client/src/client.ts
Normal file
713
parksmarter-client/src/client.ts
Normal file
|
|
@ -0,0 +1,713 @@
|
|||
/**
|
||||
* ParkSmarterClient — a typed wrapper over the ParkSmarter (IPS Group) REST API.
|
||||
*
|
||||
* Reverse-engineered from the official Android app (com.ipsgroupinc.parksmarter 4.4.0,
|
||||
* a React Native / Hermes build). All endpoint paths, header names, the app-token
|
||||
* scheme, and request/response field names were recovered from the app bundle.
|
||||
*
|
||||
* Auth model (NOT OAuth bearer):
|
||||
* - `Application_Token` : identifies the app build; required on every call.
|
||||
* - `Auth_Token` : the logged-in user's token; returned by POST /api/Auth
|
||||
* (login) in the body field `Auth_Token`, then sent as a
|
||||
* header on authenticated calls.
|
||||
* - `ParkSmarter_SessionId` : a server session id; seeded by
|
||||
* GET /api/ApplicationValidity and login, echoed on some
|
||||
* responses. Sent as a header when present.
|
||||
* - `X-Request-Id` : a fresh UUID per request.
|
||||
*
|
||||
* The client persists Auth_Token and SessionId into the provided TokenStore
|
||||
* automatically after login / bootstrap.
|
||||
*/
|
||||
import {
|
||||
DEFAULT_ENVIRONMENT,
|
||||
ENVIRONMENTS,
|
||||
Environment,
|
||||
EnvironmentName,
|
||||
} from './environments.js';
|
||||
import {
|
||||
HttpClient,
|
||||
MemoryTokenStore,
|
||||
ParkSmarterResponse,
|
||||
TokenStore,
|
||||
} from './http.js';
|
||||
import * as T from './types.js';
|
||||
|
||||
export interface ParkSmarterClientOptions {
|
||||
/** Named environment (default 'prodv2') or a fully custom Environment. */
|
||||
environment?: EnvironmentName | Environment;
|
||||
/** Token persistence. Defaults to in-memory. Use secure storage in real apps. */
|
||||
tokens?: TokenStore;
|
||||
/** Locale sent as the `localeCode` query param (default 'en'). */
|
||||
localeCode?: string;
|
||||
timeoutMs?: number;
|
||||
/** Provide a fetch impl for React Native / Node < 18 / tests. */
|
||||
fetchImpl?: typeof fetch;
|
||||
uuid?: () => string;
|
||||
}
|
||||
|
||||
function resolveEnvironment(
|
||||
env: EnvironmentName | Environment | undefined,
|
||||
): Environment {
|
||||
if (!env) return ENVIRONMENTS[DEFAULT_ENVIRONMENT];
|
||||
if (typeof env === 'string') return ENVIRONMENTS[env];
|
||||
return env;
|
||||
}
|
||||
|
||||
export class ParkSmarterClient {
|
||||
readonly http: HttpClient;
|
||||
readonly tokens: TokenStore;
|
||||
|
||||
constructor(options: ParkSmarterClientOptions = {}) {
|
||||
this.tokens = options.tokens ?? new MemoryTokenStore();
|
||||
this.http = new HttpClient({
|
||||
environment: resolveEnvironment(options.environment),
|
||||
tokens: this.tokens,
|
||||
localeCode: options.localeCode,
|
||||
timeoutMs: options.timeoutMs,
|
||||
fetchImpl: options.fetchImpl,
|
||||
uuid: options.uuid,
|
||||
});
|
||||
}
|
||||
|
||||
/** Set the locale used for the `localeCode` query param on subsequent calls. */
|
||||
setLocale(localeCode: string): void {
|
||||
this.http.setLocaleCode(localeCode);
|
||||
}
|
||||
|
||||
private unwrap<D>(p: Promise<ParkSmarterResponse<D>>): Promise<D> {
|
||||
return p.then((r) => r.data);
|
||||
}
|
||||
|
||||
/* ============================================================== */
|
||||
/* Bootstrap / app validity */
|
||||
/* ============================================================== */
|
||||
|
||||
/**
|
||||
* GET /api/ApplicationValidity — call this first. Returns feature flags,
|
||||
* maintenance/upgrade status, and a SessionId that is stored automatically.
|
||||
* Public (no Auth_Token required).
|
||||
*/
|
||||
async getApplicationValidity(): Promise<T.ApplicationValidityResponse> {
|
||||
const res = await this.http.request<T.ApplicationValidityResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/ApplicationValidity',
|
||||
query: { ApplicationToken: this.http.environment.appToken },
|
||||
includeAuthToken: false,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/* ============================================================== */
|
||||
/* Auth */
|
||||
/* ============================================================== */
|
||||
|
||||
/** POST /api/Auth — phone + password login. Persists Auth_Token & SessionId. */
|
||||
async loginWithPhone(params: T.LoginWithPhoneParams): Promise<T.AuthResponse> {
|
||||
const res = await this.http.request<T.AuthResponse>({
|
||||
method: 'POST',
|
||||
path: '/api/Auth',
|
||||
body: { UserName: params.phoneNumber, Password: params.password },
|
||||
includeAuthToken: false,
|
||||
});
|
||||
if (res.data?.Auth_Token) await this.tokens.setAuthToken(res.data.Auth_Token);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/** POST /api/Auth — Sign in with Apple. Persists Auth_Token & SessionId. */
|
||||
async loginWithApple(params: T.LoginWithAppleParams): Promise<T.AuthResponse> {
|
||||
const res = await this.http.request<T.AuthResponse>({
|
||||
method: 'POST',
|
||||
path: '/api/Auth',
|
||||
body: {
|
||||
EmailAddress: params.emailAddress,
|
||||
AppleUserId: params.appleId,
|
||||
ProviderAuth: params.providerAuth,
|
||||
ProviderIdentity: params.providerIdentity,
|
||||
},
|
||||
includeAuthToken: false,
|
||||
});
|
||||
if (res.data?.Auth_Token) await this.tokens.setAuthToken(res.data.Auth_Token);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/** Re-authenticate using a cached Auth_Token (sets it, then callers can bootstrap). */
|
||||
async loginWithCachedToken(authToken: string): Promise<void> {
|
||||
await this.tokens.setAuthToken(authToken);
|
||||
}
|
||||
|
||||
/** POST /api/Auth/Logout — invalidate the token on all devices. */
|
||||
async logoutAllDevices(): Promise<void> {
|
||||
await this.http.request({ method: 'POST', path: '/api/Auth/Logout' });
|
||||
}
|
||||
|
||||
/** Clear local tokens (client-side sign out). */
|
||||
async logoutLocal(): Promise<void> {
|
||||
await this.tokens.setAuthToken(null);
|
||||
await this.tokens.setSessionId(null);
|
||||
}
|
||||
|
||||
/* ============================================================== */
|
||||
/* Sign up, password, verification */
|
||||
/* ============================================================== */
|
||||
|
||||
/** POST /api/User — create an account. Public. */
|
||||
async signUp(params: T.SignUpParams): Promise<boolean> {
|
||||
await this.http.request({
|
||||
method: 'POST',
|
||||
path: '/api/User',
|
||||
body: {
|
||||
PersonalEmailAddress: params.emailAddress,
|
||||
PersonalPhone: params.mobilePhone,
|
||||
Password: params.password,
|
||||
},
|
||||
includeAuthToken: false,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/** POST /api/Password — request a password reset. Public. */
|
||||
async requestResetPassword(
|
||||
params: T.RequestResetPasswordParams,
|
||||
): Promise<void> {
|
||||
await this.http.request({
|
||||
method: 'POST',
|
||||
path: '/api/Password',
|
||||
body: {
|
||||
ResetPasswordType: params.reqType,
|
||||
PersonalEmailAddress: '',
|
||||
PersonalPhone: params.phoneNumber,
|
||||
},
|
||||
includeAuthToken: false,
|
||||
});
|
||||
}
|
||||
|
||||
/** PUT /api/Password — change password (authenticated). */
|
||||
async updatePassword(params: T.UpdatePasswordParams): Promise<void> {
|
||||
await this.http.request({
|
||||
method: 'PUT',
|
||||
path: '/api/Password',
|
||||
body: { OldPassword: params.oldPassword, NewPassword: params.newPassword },
|
||||
});
|
||||
}
|
||||
|
||||
/** POST /api/UserVerification — request an SMS verification code. */
|
||||
async requestVerifyUser(params: T.RequestVerifyUserParams): Promise<void> {
|
||||
await this.http.request({
|
||||
method: 'POST',
|
||||
path: '/api/UserVerification',
|
||||
body: { PhoneNumber: '1' + params.phoneNumber },
|
||||
});
|
||||
}
|
||||
|
||||
/** GET /api/UserVerification — confirm an SMS code. */
|
||||
async verifyUser(params: T.VerifyUserParams): Promise<void> {
|
||||
await this.http.request({
|
||||
method: 'GET',
|
||||
path: '/api/UserVerification',
|
||||
query: {
|
||||
PhoneNumber: '1' + params.phoneNumber,
|
||||
ConfirmationCode: params.code,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** GET /api/User?EmailAddress=… — is this email already registered? */
|
||||
async isEmailRegistered(emailAddress: string): Promise<boolean> {
|
||||
const res = await this.http.request<T.UserDetail>({
|
||||
method: 'GET',
|
||||
path: '/api/User',
|
||||
query: { EmailAddress: emailAddress },
|
||||
includeAuthToken: false,
|
||||
});
|
||||
return Boolean(res.data?.PersonalEmailAddress);
|
||||
}
|
||||
|
||||
/** GET /api/User?PhoneNumber=… — is this phone already registered? */
|
||||
async isPhoneRegistered(phoneNumber: string): Promise<boolean> {
|
||||
const res = await this.http.request<T.UserDetail>({
|
||||
method: 'GET',
|
||||
path: '/api/User',
|
||||
query: { PhoneNumber: phoneNumber },
|
||||
includeAuthToken: false,
|
||||
});
|
||||
return Boolean(res.data?.PersonalPhone);
|
||||
}
|
||||
|
||||
/* ============================================================== */
|
||||
/* User profile */
|
||||
/* ============================================================== */
|
||||
|
||||
/** GET /api/User — full profile incl. vehicles & cards (authenticated). */
|
||||
getUserDetail(): Promise<T.UserDetail> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.UserDetail>({
|
||||
method: 'GET',
|
||||
path: '/api/User',
|
||||
query: { EmailAddress: '', PhoneNumber: '' },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** PUT /api/User — update email/phone (authenticated). */
|
||||
async updateProfile(params: T.UpdateProfileParams): Promise<void> {
|
||||
await this.http.request({
|
||||
method: 'PUT',
|
||||
path: '/api/User',
|
||||
body: {
|
||||
PersonalEmailAddress: params.emailAddress,
|
||||
PersonalPhone: params.phoneNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** DELETE /api/User — request account deletion (authenticated). */
|
||||
async requestDeleteUser(): Promise<void> {
|
||||
await this.http.request({
|
||||
method: 'DELETE',
|
||||
path: '/api/User',
|
||||
query: { DeviceType: 'Android' },
|
||||
});
|
||||
}
|
||||
|
||||
/** PUT /api/Device — register/update the push device token. */
|
||||
async updateDeviceToken(
|
||||
params: T.UpdateDeviceTokenParams,
|
||||
): Promise<T.UpdateDeviceTokenResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.UpdateDeviceTokenResponse>({
|
||||
method: 'PUT',
|
||||
path: '/api/Device',
|
||||
body: {
|
||||
DeviceID: params.pushNotificationsToken,
|
||||
IMEINumber: params.imeiNumber,
|
||||
DeviceType: params.deviceType ?? '1',
|
||||
Language: params.language,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================== */
|
||||
/* Vehicles */
|
||||
/* ============================================================== */
|
||||
|
||||
/** POST /api/Vehicle — add a vehicle. */
|
||||
async addVehicle(params: T.AddVehicleParams): Promise<T.VehicleMutationResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.VehicleMutationResponse>({
|
||||
method: 'POST',
|
||||
path: '/api/Vehicle',
|
||||
body: {
|
||||
VehiclePlate: params.plate,
|
||||
VehicleState: params.state,
|
||||
VehicleAlias: params.vehicleAlias,
|
||||
IsDefault: params.isDefaultVehicle ?? false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** PUT /api/Vehicle — update a vehicle. Success when `Status === 'Success'`. */
|
||||
async updateVehicle(
|
||||
params: T.UpdateVehicleParams,
|
||||
): Promise<T.VehicleMutationResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.VehicleMutationResponse>({
|
||||
method: 'PUT',
|
||||
path: '/api/Vehicle',
|
||||
body: {
|
||||
VehiclePlate: params.plate,
|
||||
VehicleState: params.state,
|
||||
VehicleID: params.id,
|
||||
VehicleAlias: params.vehicleAlias,
|
||||
IsDefault: params.isDefaultVehicle ?? false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** DELETE /api/Vehicle?VehicleID=… */
|
||||
async deleteVehicle(vehicleId: number | string): Promise<void> {
|
||||
await this.http.request({
|
||||
method: 'DELETE',
|
||||
path: '/api/Vehicle',
|
||||
query: { VehicleID: vehicleId },
|
||||
});
|
||||
}
|
||||
|
||||
/* ============================================================== */
|
||||
/* Credit cards */
|
||||
/* ============================================================== */
|
||||
|
||||
/** POST /api/Card — add a card. */
|
||||
async addCard(params: T.AddCardParams): Promise<unknown> {
|
||||
return this.unwrap(
|
||||
this.http.request({
|
||||
method: 'POST',
|
||||
path: '/api/Card',
|
||||
body: this.buildCardBody(params),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** POST /api/Card — update a card (full replace). */
|
||||
async updateCard(params: T.AddCardParams): Promise<unknown> {
|
||||
return this.unwrap(
|
||||
this.http.request({
|
||||
method: 'POST',
|
||||
path: '/api/Card',
|
||||
body: this.buildCardBody(params),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** PUT /api/Card — set (or unset) a card as default. */
|
||||
async setCardDefault(params: T.SetDefaultCardParams): Promise<unknown> {
|
||||
return this.unwrap(
|
||||
this.http.request({
|
||||
method: 'PUT',
|
||||
path: '/api/Card',
|
||||
body: { CCID: params.id, CCDefault: params.isDefaultCard },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** DELETE /api/Card?CCID=… */
|
||||
async deleteCard(cardId: number | string): Promise<void> {
|
||||
await this.http.request({
|
||||
method: 'DELETE',
|
||||
path: '/api/Card',
|
||||
query: { CCID: cardId },
|
||||
});
|
||||
}
|
||||
|
||||
private buildCardBody(p: T.AddCardParams) {
|
||||
return {
|
||||
CCNumber: p.cardNumber,
|
||||
CCAlias: p.alias,
|
||||
CCExpDate: p.expDate,
|
||||
CCZip: p.zipCode,
|
||||
CCDefault: p.isDefaultCard ?? false,
|
||||
ParentCCID: p.id,
|
||||
};
|
||||
}
|
||||
|
||||
/* ============================================================== */
|
||||
/* Meters / zones */
|
||||
/* ============================================================== */
|
||||
|
||||
/** GET /api/Meter?Lat=&Long= — meters near a coordinate. */
|
||||
getMetersByLocation(loc: T.LatLng): Promise<T.MetersResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.MetersResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/Meter',
|
||||
query: { Lat: loc.latitude, Long: loc.longitude },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/MeterList?Lat=&Long= — limited meter list near a coordinate. */
|
||||
getLimitedMetersByLocation(loc: T.LatLng): Promise<T.MetersResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.MetersResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/MeterList',
|
||||
query: { Lat: loc.latitude, Long: loc.longitude },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/Meter?ZoneName=… */
|
||||
getMetersByZoneName(zoneName: string): Promise<T.MetersResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.MetersResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/Meter',
|
||||
query: { ZoneName: zoneName },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/Meter?Query=… — zone or space name search. */
|
||||
searchMetersByZoneOrSpace(query: string): Promise<T.MetersResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.MetersResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/Meter',
|
||||
query: { Query: query },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/Meter?TerminalSerNo=… */
|
||||
getMetersBySerialNumber(serialNumber: string): Promise<T.MetersResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.MetersResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/Meter',
|
||||
query: { TerminalSerNo: serialNumber },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/Meter?ScannerCode=… (e.g. from a scanned QR/barcode). */
|
||||
getMetersByScannerCode(scannerCode: string): Promise<T.MetersResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.MetersResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/Meter',
|
||||
query: { ScannerCode: scannerCode },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/ParkingLogix — nearby parking lots with occupancy. */
|
||||
getParkingLots(): Promise<T.ParkingLotsResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.ParkingLotsResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/ParkingLogix',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================== */
|
||||
/* Estimates */
|
||||
/* ============================================================== */
|
||||
|
||||
/** GET /api/ParkingEstimateMulti — multi-duration price ladder. */
|
||||
getParkingEstimateMulti(
|
||||
params: T.MultiEstimateParams,
|
||||
): Promise<T.MultiEstimateResponse> {
|
||||
const query: Record<string, unknown> = {
|
||||
ZoneID: params.zoneId,
|
||||
SpaceID: params.spaceId,
|
||||
CustomerID: params.customerId,
|
||||
MinCreditAmount: params.minCreditAmount,
|
||||
VehicleID: params.vehicleId,
|
||||
};
|
||||
if (params.bleEncBytes) query.BleEncBytesString = params.bleEncBytes;
|
||||
return this.unwrap(
|
||||
this.http.request<T.MultiEstimateResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/ParkingEstimateMulti',
|
||||
query,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/ParkingEstimate — single-duration price. */
|
||||
getParkingEstimateSingle(
|
||||
params: T.SingleEstimateParams,
|
||||
): Promise<T.SingleEstimateResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.SingleEstimateResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/ParkingEstimate',
|
||||
query: {
|
||||
ZoneID: params.zoneId,
|
||||
SpaceID: params.spaceId,
|
||||
CustomerID: params.customerId,
|
||||
ParkingDuration: params.durationInMinutes,
|
||||
VehicleID: params.vehicleId,
|
||||
CCID: params.creditCardId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/ParkingEstimateItems — item-based price options. */
|
||||
getParkingEstimateItems(
|
||||
params: T.ItemsEstimateParams,
|
||||
): Promise<T.ItemsEstimateResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.ItemsEstimateResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/ParkingEstimateItems',
|
||||
query: {
|
||||
ZoneID: params.zoneId,
|
||||
SpaceID: params.spaceId,
|
||||
CustomerID: params.customerId,
|
||||
VehicleID: params.vehicleId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================== */
|
||||
/* Parking sessions */
|
||||
/* ============================================================== */
|
||||
|
||||
/** POST /api/Session — start/pay for a parking session. */
|
||||
startParkingSession(
|
||||
params: T.StartParkingSessionParams,
|
||||
): Promise<T.StartParkingSessionResponse> {
|
||||
const fee = params.transactionFee ?? 0;
|
||||
const body: Record<string, unknown> = {
|
||||
CCID: String(params.creditCardId),
|
||||
Amount: (params.parkingCost + fee).toFixed(2),
|
||||
SpaceID: params.spaceId,
|
||||
StartTime: params.startTime,
|
||||
EndTime: params.endTime,
|
||||
CustomerID: params.customerId,
|
||||
VehicleID: String(params.vehicleId),
|
||||
TimePurchased: String(params.minutesToPurchase),
|
||||
ParkingCost: params.parkingCost.toFixed(2),
|
||||
TransactionFee: fee.toFixed(2),
|
||||
ZoneID: params.zoneId,
|
||||
MinCreditAmount:
|
||||
params.minCreditAmount != null
|
||||
? params.minCreditAmount.toFixed(2)
|
||||
: undefined,
|
||||
MeterTypeId: params.meterTypeId,
|
||||
};
|
||||
if (params.bleEncBytes) body.BleEncBytes = params.bleEncBytes;
|
||||
return this.unwrap(
|
||||
this.http.request<T.StartParkingSessionResponse>({
|
||||
method: 'POST',
|
||||
path: '/api/Session',
|
||||
body,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/ParkingSession — currently active sessions. */
|
||||
getActiveParkingSessions(): Promise<T.ActiveSessionsResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.ActiveSessionsResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/ParkingSession',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/Session — past sessions (paged). */
|
||||
getPastParkingSessions(
|
||||
params: T.PastSessionsParams = {},
|
||||
): Promise<T.PastSessionsResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.PastSessionsResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/Session',
|
||||
query: {
|
||||
CurrentPage: params.currentPage ?? '',
|
||||
PageSize: params.pageSize ?? '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================== */
|
||||
/* Receipts */
|
||||
/* ============================================================== */
|
||||
|
||||
/** GET /api/ParkingReceipt?TransactionID=… */
|
||||
getParkingReceipt(transactionId: number | string): Promise<T.ReceiptResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.ReceiptResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/ParkingReceipt',
|
||||
query: { TransactionID: transactionId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** POST /api/ParkingReceipt?Id=… — email a receipt to the account holder. */
|
||||
async emailParkingReceipt(receiptId: number | string): Promise<void> {
|
||||
await this.http.request({
|
||||
method: 'POST',
|
||||
path: '/api/ParkingReceipt',
|
||||
query: { Id: receiptId },
|
||||
});
|
||||
}
|
||||
|
||||
/* ============================================================== */
|
||||
/* Notification settings */
|
||||
/* ============================================================== */
|
||||
|
||||
/** GET /api/Setting — raw notification settings bitmask. */
|
||||
getNotificationSettings(): Promise<T.RawSettingsResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.RawSettingsResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/Setting',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/Setting — update notification settings.
|
||||
* `userSettings` is the numeric bitmask (as a string) the app computes via reduceSettings.
|
||||
*/
|
||||
async setNotificationSettings(userSettings: string | number): Promise<void> {
|
||||
await this.http.request({
|
||||
method: 'POST',
|
||||
path: '/api/Setting',
|
||||
body: { UserSettings: String(userSettings) },
|
||||
});
|
||||
}
|
||||
|
||||
/* ============================================================== */
|
||||
/* Static content */
|
||||
/* ============================================================== */
|
||||
|
||||
/**
|
||||
* GET /api/State?ID=… — states/regions under a parent id.
|
||||
* The `ID` param is required — the route 404s without it (confirmed against prod).
|
||||
*/
|
||||
getStates(id: number | string): Promise<T.StatesResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.StatesResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/State',
|
||||
query: { ID: id },
|
||||
includeAuthToken: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/ParkSmarterAbout — `{ Value }`. */
|
||||
getAbout(): Promise<T.ValueResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.ValueResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/ParkSmarterAbout',
|
||||
includeAuthToken: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/ParkSmarterFAQ — `{ FAQs: [...] }`. */
|
||||
getFAQ(): Promise<T.FAQResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.FAQResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/ParkSmarterFAQ',
|
||||
includeAuthToken: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/ParkSmarterPrivacyPolicies — `{ Value }`. */
|
||||
getPrivacyPolicy(): Promise<T.ValueResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.ValueResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/ParkSmarterPrivacyPolicies',
|
||||
includeAuthToken: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/ParkSmarterTerms — `{ Value }`. */
|
||||
getTerms(): Promise<T.ValueResponse> {
|
||||
return this.unwrap(
|
||||
this.http.request<T.ValueResponse>({
|
||||
method: 'GET',
|
||||
path: '/api/ParkSmarterTerms',
|
||||
includeAuthToken: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
58
parksmarter-client/src/environments.ts
Normal file
58
parksmarter-client/src/environments.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* ParkSmarter backend environments.
|
||||
*
|
||||
* Recovered from the official app (com.ipsgroupinc.parksmarter 4.4.0).
|
||||
* Each environment pairs a base URL with an application token that the server
|
||||
* requires on every request via the `Application_Token` header.
|
||||
*
|
||||
* `prodv2` is the environment the shipping app defaults to.
|
||||
*/
|
||||
export type EnvironmentName =
|
||||
| 'dev'
|
||||
| 'stage'
|
||||
| 'test'
|
||||
| 'prodv1'
|
||||
| 'prodv2'
|
||||
| 'prodv3';
|
||||
|
||||
export interface Environment {
|
||||
name: EnvironmentName;
|
||||
baseUrl: string;
|
||||
appToken: string;
|
||||
}
|
||||
|
||||
export const ENVIRONMENTS: Record<EnvironmentName, Environment> = {
|
||||
dev: {
|
||||
name: 'dev',
|
||||
baseUrl: 'https://dev-parksmarter-api.ipsmeters.com',
|
||||
appToken: 'ED09B2F6-BE30-4C92-9204-7D02026CAAE7',
|
||||
},
|
||||
stage: {
|
||||
name: 'stage',
|
||||
baseUrl: 'https://staging-parksmarter-api.ipsmeters.com',
|
||||
appToken: 'AC657B81-121E-42E8-B6BF-5C2C668B4E00',
|
||||
},
|
||||
test: {
|
||||
name: 'test',
|
||||
baseUrl: 'https://testing-parksmarter-api.ipsmeters.com',
|
||||
appToken: 'AC657B81-121E-42E8-B6BF-5C2C668B4E00',
|
||||
},
|
||||
prodv1: {
|
||||
name: 'prodv1',
|
||||
baseUrl: 'https://api.parksmarter.com',
|
||||
appToken: '98774898-E21E-4548-B513-FA7211ABA442',
|
||||
},
|
||||
prodv2: {
|
||||
name: 'prodv2',
|
||||
baseUrl: 'https://apiv2.parksmarter.com',
|
||||
appToken: 'B66EEDDA-B618-4926-B8A0-F5B58397EEBA',
|
||||
},
|
||||
prodv3: {
|
||||
name: 'prodv3',
|
||||
baseUrl: 'https://apiv3.parksmarter.com',
|
||||
appToken: '98774898-E21E-4548-B513-FA7211ABA442',
|
||||
},
|
||||
};
|
||||
|
||||
/** The environment the official app ships pointed at. */
|
||||
export const DEFAULT_ENVIRONMENT: EnvironmentName = 'prodv2';
|
||||
252
parksmarter-client/src/http.ts
Normal file
252
parksmarter-client/src/http.ts
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
/**
|
||||
* Transport layer for the ParkSmarter API.
|
||||
*
|
||||
* This mirrors the request pipeline used by the official app:
|
||||
* - Base URL + query string are concatenated onto the endpoint path.
|
||||
* - Query params are serialized as `?k=encodeURIComponent(v)&...`.
|
||||
* - Headers:
|
||||
* Application_Token always (identifies the app build)
|
||||
* X-Request-Id always (a fresh UUID per request)
|
||||
* Content-Type: application/json on POST/PUT
|
||||
* Auth_Token when the endpoint requires an authenticated user
|
||||
* ParkSmarter_SessionId when a server session id is available
|
||||
* - The user auth token and session id are NOT HTTP bearer tokens; they are
|
||||
* custom headers named exactly `Auth_Token` and `ParkSmarter_SessionId`.
|
||||
*/
|
||||
import { Environment } from './environments.js';
|
||||
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
|
||||
export interface TokenStore {
|
||||
/** Current user auth token, or null if signed out. */
|
||||
getAuthToken(): string | null | Promise<string | null>;
|
||||
setAuthToken(token: string | null): void | Promise<void>;
|
||||
/** Current server session id, or null. */
|
||||
getSessionId(): string | null | Promise<string | null>;
|
||||
setSessionId(sessionId: string | null): void | Promise<void>;
|
||||
}
|
||||
|
||||
/** Simple in-memory token store. Swap for expo-secure-store / localStorage in real apps. */
|
||||
export class MemoryTokenStore implements TokenStore {
|
||||
private authToken: string | null = null;
|
||||
private sessionId: string | null = null;
|
||||
getAuthToken() {
|
||||
return this.authToken;
|
||||
}
|
||||
setAuthToken(token: string | null) {
|
||||
this.authToken = token;
|
||||
}
|
||||
getSessionId() {
|
||||
return this.sessionId;
|
||||
}
|
||||
setSessionId(sessionId: string | null) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RequestOptions {
|
||||
method: HttpMethod;
|
||||
/** Endpoint path, e.g. `/api/Auth`. */
|
||||
path: string;
|
||||
query?: Record<string, unknown> | undefined;
|
||||
body?: unknown;
|
||||
/** Send the `Auth_Token` header (default: true except for auth/public endpoints). */
|
||||
includeAuthToken?: boolean;
|
||||
/** Send the `ParkSmarter_SessionId` header when available (default: true). */
|
||||
includeSessionId?: boolean;
|
||||
/** Abort signal (also drives the per-request timeout). */
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface ParkSmarterResponse<T> {
|
||||
status: number;
|
||||
data: T;
|
||||
headers: Headers;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
/** Thrown for non-2xx responses. `body` is the parsed server payload when JSON. */
|
||||
export class ParkSmarterApiError extends Error {
|
||||
status: number;
|
||||
body: unknown;
|
||||
requestId: string;
|
||||
/** Server-provided message when present (server uses PascalCase `Message`). */
|
||||
serverMessage?: string;
|
||||
constructor(status: number, body: unknown, requestId: string) {
|
||||
const serverMessage =
|
||||
body && typeof body === 'object'
|
||||
? (body as Record<string, unknown>).Message ??
|
||||
(body as Record<string, unknown>).message
|
||||
: undefined;
|
||||
super(
|
||||
`ParkSmarter API error ${status}` +
|
||||
(serverMessage ? `: ${serverMessage}` : ''),
|
||||
);
|
||||
this.name = 'ParkSmarterApiError';
|
||||
this.status = status;
|
||||
this.body = body;
|
||||
this.requestId = requestId;
|
||||
if (typeof serverMessage === 'string') this.serverMessage = serverMessage;
|
||||
}
|
||||
}
|
||||
|
||||
export interface HttpClientConfig {
|
||||
environment: Environment;
|
||||
tokens: TokenStore;
|
||||
/** BCP-47-ish locale code sent as the `localeCode` query param (default: 'en'). */
|
||||
localeCode?: string;
|
||||
/** Per-request timeout in ms (default: 30000). */
|
||||
timeoutMs?: number;
|
||||
/** Override fetch (e.g. for React Native or tests). Defaults to global fetch. */
|
||||
fetchImpl?: typeof fetch;
|
||||
/** Override UUID generation. Defaults to crypto.randomUUID when available. */
|
||||
uuid?: () => string;
|
||||
}
|
||||
|
||||
function defaultUuid(): string {
|
||||
const c = (globalThis as { crypto?: Crypto }).crypto;
|
||||
if (c && typeof c.randomUUID === 'function') return c.randomUUID();
|
||||
// RFC4122-ish fallback (non-crypto) for older runtimes.
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (ch) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = ch === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function serializeQuery(query: Record<string, unknown>): string {
|
||||
const parts: string[] = [];
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
}
|
||||
return parts.length ? `?${parts.join('&')}` : '';
|
||||
}
|
||||
|
||||
export class HttpClient {
|
||||
private cfg: Required<Omit<HttpClientConfig, 'environment' | 'tokens'>> &
|
||||
Pick<HttpClientConfig, 'environment' | 'tokens'>;
|
||||
|
||||
constructor(config: HttpClientConfig) {
|
||||
this.cfg = {
|
||||
environment: config.environment,
|
||||
tokens: config.tokens,
|
||||
localeCode: config.localeCode ?? 'en',
|
||||
timeoutMs: config.timeoutMs ?? 30000,
|
||||
fetchImpl: config.fetchImpl ?? globalThis.fetch?.bind(globalThis),
|
||||
uuid: config.uuid ?? defaultUuid,
|
||||
};
|
||||
if (!this.cfg.fetchImpl) {
|
||||
throw new Error(
|
||||
'No fetch implementation available. Pass config.fetchImpl (e.g. node-fetch, or a polyfill).',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get environment(): Environment {
|
||||
return this.cfg.environment;
|
||||
}
|
||||
|
||||
get tokens(): TokenStore {
|
||||
return this.cfg.tokens;
|
||||
}
|
||||
|
||||
setLocaleCode(localeCode: string) {
|
||||
this.cfg.localeCode = localeCode;
|
||||
}
|
||||
|
||||
async request<T>(opts: RequestOptions): Promise<ParkSmarterResponse<T>> {
|
||||
const { method } = opts;
|
||||
const requestId = this.cfg.uuid();
|
||||
|
||||
// The app appends `localeCode` to the query params of essentially every call.
|
||||
const query: Record<string, unknown> = {
|
||||
localeCode: this.cfg.localeCode,
|
||||
...(opts.query ?? {}),
|
||||
};
|
||||
const url =
|
||||
this.cfg.environment.baseUrl + opts.path + serializeQuery(query);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Application_Token: this.cfg.environment.appToken,
|
||||
'X-Request-Id': requestId,
|
||||
Accept: 'application/json',
|
||||
};
|
||||
if (method === 'POST' || method === 'PUT') {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
const includeAuthToken = opts.includeAuthToken ?? true;
|
||||
const includeSessionId = opts.includeSessionId ?? true;
|
||||
|
||||
if (includeAuthToken) {
|
||||
const authToken = await this.cfg.tokens.getAuthToken();
|
||||
if (authToken) headers['Auth_Token'] = authToken;
|
||||
}
|
||||
if (includeSessionId) {
|
||||
const sessionId = await this.cfg.tokens.getSessionId();
|
||||
if (sessionId) headers['ParkSmarter_SessionId'] = sessionId;
|
||||
}
|
||||
|
||||
// Timeout wired through an AbortController, honoring any caller-supplied signal.
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), this.cfg.timeoutMs);
|
||||
if (opts.signal) {
|
||||
if (opts.signal.aborted) controller.abort();
|
||||
else opts.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
||||
}
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await this.cfg.fetchImpl(url, {
|
||||
method,
|
||||
headers,
|
||||
body:
|
||||
opts.body !== undefined && opts.body !== null
|
||||
? JSON.stringify(opts.body)
|
||||
: undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
let data: unknown = undefined;
|
||||
if (text && text.trim().length) {
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
data = text;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the token store fresh from what the server echoes back:
|
||||
// - SessionId is returned on bootstrap/login.
|
||||
// - A fresh Auth_Token may be returned either at the top level (login) or
|
||||
// inside the common `Response: { Auth_Token, Message, Status }` envelope
|
||||
// (a rolling token-refresh mechanism). Only overwrite on a non-empty value.
|
||||
if (data && typeof data === 'object') {
|
||||
const obj = data as Record<string, unknown>;
|
||||
const envelope = obj.Response as Record<string, unknown> | undefined;
|
||||
|
||||
const sessionId = obj.SessionId;
|
||||
if (typeof sessionId === 'string' && sessionId) {
|
||||
await this.cfg.tokens.setSessionId(sessionId);
|
||||
}
|
||||
|
||||
const refreshed =
|
||||
(typeof obj.Auth_Token === 'string' && obj.Auth_Token) ||
|
||||
(envelope && typeof envelope.Auth_Token === 'string' && envelope.Auth_Token);
|
||||
if (refreshed) {
|
||||
await this.cfg.tokens.setAuthToken(refreshed);
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new ParkSmarterApiError(res.status, data ?? text, requestId);
|
||||
}
|
||||
|
||||
return { status: res.status, data: data as T, headers: res.headers, requestId };
|
||||
}
|
||||
}
|
||||
20
parksmarter-client/src/index.ts
Normal file
20
parksmarter-client/src/index.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export { ParkSmarterClient } from './client.js';
|
||||
export type { ParkSmarterClientOptions } from './client.js';
|
||||
export {
|
||||
ENVIRONMENTS,
|
||||
DEFAULT_ENVIRONMENT,
|
||||
} from './environments.js';
|
||||
export type { Environment, EnvironmentName } from './environments.js';
|
||||
export {
|
||||
HttpClient,
|
||||
MemoryTokenStore,
|
||||
ParkSmarterApiError,
|
||||
} from './http.js';
|
||||
export type {
|
||||
TokenStore,
|
||||
RequestOptions,
|
||||
ParkSmarterResponse,
|
||||
HttpMethod,
|
||||
HttpClientConfig,
|
||||
} from './http.js';
|
||||
export * from './types.js';
|
||||
624
parksmarter-client/src/types.ts
Normal file
624
parksmarter-client/src/types.ts
Normal file
|
|
@ -0,0 +1,624 @@
|
|||
/**
|
||||
* ParkSmarter API data models.
|
||||
*
|
||||
* Two casing worlds exist:
|
||||
* - This client's *inputs* use friendly camelCase (see the `*Params` types).
|
||||
* - The server speaks PascalCase ("PSJSON"). Response models below therefore use
|
||||
* the exact server field spellings recovered from the app. Response interfaces
|
||||
* carry an index signature because not every field is guaranteed on every call;
|
||||
* the well-known fields are typed explicitly. Run a live capture to lock down
|
||||
* the optional tail for a given endpoint if you need exhaustiveness.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The common status/token-refresh envelope embedded in most authenticated
|
||||
* responses as `Response`. `Auth_Token` here is usually null but, when present,
|
||||
* is a rolling refresh of the user token (the client stores it automatically).
|
||||
* CONFIRMED via live capture.
|
||||
*/
|
||||
export interface ResponseEnvelope {
|
||||
Auth_Token: string | null;
|
||||
Message: string | null;
|
||||
Status: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Casing note (CONFIRMED via live capture): the meter/zone *response* uses
|
||||
* `ZoneId` / `SpaceId` / `CustomerId` (lowercase "d"), while estimate & session
|
||||
* *requests* expect `ZoneID` / `SpaceID` / `CustomerID` (capital "ID"). When you
|
||||
* feed a captured Zone into an estimate/session, read `zone.CustomerId` /
|
||||
* `space.SpaceId` and pass them as the client's camelCase params — the client
|
||||
* emits the capital-ID request spelling for you.
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Auth & account */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export interface LoginWithPhoneParams {
|
||||
/** Phone number, digits only (no country code prefix). */
|
||||
phoneNumber: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginWithAppleParams {
|
||||
emailAddress: string;
|
||||
appleId: string;
|
||||
providerAuth: string;
|
||||
providerIdentity: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/Auth response (login). CONFIRMED: `{ Status, Message, Auth_Token }`.
|
||||
* Auth_Token is persisted automatically; SessionId is delivered separately (by
|
||||
* bootstrap / headers) and also persisted.
|
||||
*/
|
||||
export interface AuthResponse {
|
||||
Status?: string;
|
||||
Message?: string;
|
||||
Auth_Token?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SignUpParams {
|
||||
emailAddress: string;
|
||||
/** Phone number, digits only. */
|
||||
mobilePhone: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RequestResetPasswordParams {
|
||||
/** Server `ResetPasswordType`; the app passes a numeric/string reason code. */
|
||||
reqType: string | number;
|
||||
/** Phone number, digits only. */
|
||||
phoneNumber: string;
|
||||
}
|
||||
|
||||
export interface UpdatePasswordParams {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export interface UpdateProfileParams {
|
||||
emailAddress: string;
|
||||
/** Phone number, digits only. */
|
||||
phoneNumber: string;
|
||||
}
|
||||
|
||||
export interface RequestVerifyUserParams {
|
||||
/** Phone number, digits only. The client prefixes country code "1". */
|
||||
phoneNumber: string;
|
||||
}
|
||||
|
||||
export interface VerifyUserParams {
|
||||
/** Phone number, digits only. The client prefixes country code "1". */
|
||||
phoneNumber: string;
|
||||
/** SMS confirmation code. */
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface UpdateDeviceTokenParams {
|
||||
/** Push token (server field `DeviceID`). */
|
||||
pushNotificationsToken: string;
|
||||
imeiNumber?: string;
|
||||
/** Defaults to '1' (Android) as the app hardcodes. */
|
||||
deviceType?: string;
|
||||
/** BCP-47 language, defaults to the client locale. */
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export interface UpdateDeviceTokenResponse {
|
||||
Status?: string;
|
||||
Message?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** GET /api/User response. CONFIRMED via live capture. */
|
||||
export interface UserDetail {
|
||||
PersonalEmailAddress?: string;
|
||||
/** Digits-only phone as a string. */
|
||||
PersonalPhone?: string;
|
||||
VehicleDetails?: VehicleDetail[];
|
||||
CreditCardDetails?: CreditCardDetail[];
|
||||
/** Numeric flag (0/1), not a boolean. */
|
||||
OffersOptIn?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Vehicles */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** CONFIRMED via live capture. Note `IsDefault` is a string ("true"/"false"), not a boolean. */
|
||||
export interface VehicleDetail {
|
||||
VehicleID?: number;
|
||||
VehiclePlate?: string;
|
||||
VehicleState?: string;
|
||||
VehicleAlias?: string;
|
||||
/** Server returns this as a string, e.g. "true" / "false". */
|
||||
IsDefault?: string;
|
||||
/** Per-item status fields, usually null on reads. */
|
||||
Status?: string | null;
|
||||
Message?: string | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AddVehicleParams {
|
||||
plate: string;
|
||||
state: string;
|
||||
vehicleAlias?: string;
|
||||
isDefaultVehicle?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateVehicleParams extends AddVehicleParams {
|
||||
id: number | string;
|
||||
}
|
||||
|
||||
export interface VehicleMutationResponse {
|
||||
Status?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Credit cards */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* CONFIRMED via live capture. The PAN is never returned — only `CCFirstSix` +
|
||||
* `CCLastFour`. `CCDefault` comes back as a string ("true"/"false"). Note the
|
||||
* response uses `ParentPBPCardId`, whereas the add/update *request* uses `ParentCCID`.
|
||||
*/
|
||||
export interface CreditCardDetail {
|
||||
CCID?: number;
|
||||
CCAlias?: string;
|
||||
/** First 6 digits (BIN), numeric string. */
|
||||
CCFirstSix?: string;
|
||||
/** Last 4 digits, numeric string. */
|
||||
CCLastFour?: string;
|
||||
CCExpDate?: string;
|
||||
CCZip?: string;
|
||||
/** String "true"/"false", not a boolean. */
|
||||
CCDefault?: string;
|
||||
ParentPBPCardId?: number | null;
|
||||
/** Per-item status fields, usually null on reads. */
|
||||
Status?: string | null;
|
||||
Message?: string | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AddCardParams {
|
||||
cardNumber: string;
|
||||
alias?: string;
|
||||
/** Expiry, format as the app sends it (e.g. "MM/YY"). */
|
||||
expDate: string;
|
||||
zipCode: string;
|
||||
isDefaultCard?: boolean;
|
||||
/** Present when editing an existing card (server `ParentCCID`). */
|
||||
id?: number | string;
|
||||
}
|
||||
|
||||
export interface SetDefaultCardParams {
|
||||
id: number | string;
|
||||
isDefaultCard: boolean;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Meters / zones */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export interface LatLng {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
/** A rate/time policy slot within a space. CONFIRMED via live capture. */
|
||||
export interface SpacePolicy {
|
||||
CurrentSlot?: boolean;
|
||||
DayNumber?: number;
|
||||
DisplayString?: string;
|
||||
StartDateTime?: string;
|
||||
EndDateTime?: string;
|
||||
StartTimeDisplay?: string;
|
||||
EndTimeDisplay?: string;
|
||||
EventRateFlag?: boolean;
|
||||
MaxTime?: number;
|
||||
MessageHeader?: string;
|
||||
MessageText?: string;
|
||||
Rate?: number;
|
||||
RateType?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single space within a zone. CONFIRMED via live capture.
|
||||
* Note the id is `SpaceId` (lowercase "d") here; estimate/session requests want
|
||||
* the capital-ID `SpaceID` (the client maps camelCase params to that spelling).
|
||||
*/
|
||||
export interface Space {
|
||||
SpaceId?: number;
|
||||
SpaceName?: string;
|
||||
SSPMSpaceId?: number;
|
||||
MeterPurchaseType?: number;
|
||||
OccupancyStatus?: number;
|
||||
Policies?: SpacePolicy[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* A parking zone/meter as returned inside `Zones`. CONFIRMED via live capture.
|
||||
* `ScannerCode` is the value encoded in kiosk QR codes; `TerminalSerNo` is the
|
||||
* printed serial. Both drive the scan / lookup features.
|
||||
*/
|
||||
export interface Zone {
|
||||
ZoneId?: number;
|
||||
ZoneGuid?: string;
|
||||
ZoneName?: string;
|
||||
ZoneLocation?: string;
|
||||
ZonePlates?: unknown;
|
||||
/** Printed serial number (numeric string). */
|
||||
TerminalSerNo?: string;
|
||||
/** QR-encoded scanner code. */
|
||||
ScannerCode?: string;
|
||||
Lat?: number;
|
||||
Long?: number;
|
||||
CustomerId?: number;
|
||||
CustomerName?: string;
|
||||
/** Minutes offset / IANA-less numeric zone id as returned. */
|
||||
TimeZone?: number;
|
||||
DSTAdjust?: number;
|
||||
CityLogo?: string;
|
||||
DepartmentLogo?: string;
|
||||
BackgroundColor?: string;
|
||||
/** Returned as a numeric string. */
|
||||
ForegroundColor?: string;
|
||||
MinimumAmount?: number;
|
||||
MaxTime?: number;
|
||||
MeterType?: string;
|
||||
MeterTypeId?: number;
|
||||
/** Base rate, returned as a string. */
|
||||
Rate?: string;
|
||||
ProgressiveRate?: number;
|
||||
PercentageFull?: number;
|
||||
IsAllowOverAir?: boolean;
|
||||
IsAllowBLE?: boolean;
|
||||
IsExtension?: boolean;
|
||||
IsPaid?: boolean;
|
||||
Spaces?: Space[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** GET /api/Meter and /api/MeterList. CONFIRMED via live capture. */
|
||||
export interface MetersResponse {
|
||||
Zones?: Zone[] | null;
|
||||
Response?: ResponseEnvelope;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Parking lots (ParkingLogix) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** One level/section within a ParkingLogix lot. CONFIRMED via live capture. */
|
||||
export interface ParkingLotDetail {
|
||||
ParkingLogixLotDetailId?: number;
|
||||
ParkingLogixLotId?: number;
|
||||
Level?: number;
|
||||
TotalSpaces?: number;
|
||||
FreeSpaces?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** CONFIRMED via live capture. Per-level occupancy lives in ParkingLogixLotDetails. */
|
||||
export interface ParkingLot {
|
||||
ParkingLogixLotId?: number;
|
||||
LotName?: string;
|
||||
LocationAddress?: string;
|
||||
Lat?: number;
|
||||
Long?: number;
|
||||
TotalSpaces?: number;
|
||||
DefaultFree?: boolean;
|
||||
ParkingLogixLotDetails?: ParkingLotDetail[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ParkingLotsResponse {
|
||||
ParkingLogixLots?: ParkingLot[] | null;
|
||||
Response?: ResponseEnvelope;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Estimates */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export interface MultiEstimateParams {
|
||||
zoneId: number | string;
|
||||
spaceId: number | string;
|
||||
customerId: number | string;
|
||||
/** Zone minimum credit (server `MinCreditAmount`). */
|
||||
minCreditAmount?: number | string;
|
||||
vehicleId: number | string;
|
||||
/** Optional BLE encrypted bytes string. */
|
||||
bleEncBytes?: string;
|
||||
}
|
||||
|
||||
export interface SingleEstimateParams {
|
||||
zoneId: number | string;
|
||||
spaceId: number | string;
|
||||
customerId: number | string;
|
||||
/** Server `ParkingDuration`, in minutes. */
|
||||
durationInMinutes: number;
|
||||
vehicleId: number | string;
|
||||
/** Server `CCID` — credit card id. */
|
||||
creditCardId: number | string;
|
||||
}
|
||||
|
||||
export interface ItemsEstimateParams {
|
||||
zoneId: number | string;
|
||||
spaceId: number | string;
|
||||
customerId: number | string;
|
||||
vehicleId: number | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* One priced duration option. CONFIRMED via live capture.
|
||||
* Money/id fields come back as numeric *strings* here.
|
||||
*/
|
||||
export interface ParkingDetail {
|
||||
Minutes?: number;
|
||||
StartTime?: string;
|
||||
EndTime?: string;
|
||||
ParkingCost?: string;
|
||||
TransactionFee?: string;
|
||||
CurrentAmountPaid?: number;
|
||||
CustomerID?: string;
|
||||
CustomerName?: string;
|
||||
Zone?: string;
|
||||
ZoneID?: string;
|
||||
Space?: string;
|
||||
SpaceID?: string;
|
||||
VehicleID?: string;
|
||||
VehicleNumber?: string | null;
|
||||
CCNumber?: string | null;
|
||||
PricingToken?: string | null;
|
||||
TimeZone?: number;
|
||||
DSTAdjust?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** GET /api/ParkingEstimateMulti. CONFIRMED via live capture (a full price ladder). */
|
||||
export interface MultiEstimateResponse {
|
||||
ParkingDetail?: ParkingDetail[];
|
||||
MinTime?: number;
|
||||
MaxTime?: number;
|
||||
TimeRemaining?: number;
|
||||
JumpRateEnabled?: boolean;
|
||||
JumpRateThreshold?: number;
|
||||
JumpRateSetValue?: number;
|
||||
Response?: ResponseEnvelope;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** GET /api/ParkingEstimate. CONFIRMED via live capture (a single priced option). */
|
||||
export interface SingleEstimateResponse {
|
||||
ParkingDetail?: ParkingDetail;
|
||||
MinTime?: number;
|
||||
MaxTime?: number;
|
||||
CurrentCustomerDateTime?: string;
|
||||
Response?: ResponseEnvelope;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** GET /api/ParkingEstimateItems. CONFIRMED via live capture. */
|
||||
export interface ItemsEstimateResponse {
|
||||
ZoneID?: number;
|
||||
ZoneName?: string;
|
||||
SpaceID?: number;
|
||||
SpaceName?: string;
|
||||
CustomerID?: number;
|
||||
CustomerName?: string;
|
||||
PurchaseMode?: string;
|
||||
Items?: unknown[] | null;
|
||||
TimeStampUTC?: number;
|
||||
TimeZone?: number;
|
||||
DSTAdjust?: number;
|
||||
Response?: ResponseEnvelope;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Parking sessions */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export interface StartParkingSessionParams {
|
||||
creditCardId: number | string;
|
||||
spaceId: number | string;
|
||||
customerId: number | string;
|
||||
zoneId: number | string;
|
||||
vehicleId: number | string;
|
||||
/** ISO/formatted start time as the server expects. */
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
/** Minutes purchased (server `TimePurchased`). */
|
||||
minutesToPurchase: number;
|
||||
/** Base cost, dollars (server `ParkingCost`). */
|
||||
parkingCost: number;
|
||||
/** Fee, dollars (server `TransactionFee`). Defaults to 0. */
|
||||
transactionFee?: number;
|
||||
/** Zone minimum credit (server `MinCreditAmount`). */
|
||||
minCreditAmount?: number;
|
||||
meterTypeId: number | string;
|
||||
/** Optional BLE encrypted bytes string. */
|
||||
bleEncBytes?: string;
|
||||
}
|
||||
|
||||
export interface StartParkingSessionResponse {
|
||||
BleEncBytes?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** UNCONFIRMED element shape (no active sessions on the test account). */
|
||||
export interface ActiveSession {
|
||||
TransactionID?: number | string;
|
||||
ZoneName?: string;
|
||||
SpaceName?: string;
|
||||
Space?: string;
|
||||
StartTime?: string;
|
||||
EndTime?: string;
|
||||
StartTimeDisplay?: string;
|
||||
EndTimeDisplay?: string;
|
||||
TimeRemaining?: number | string;
|
||||
VehiclePlate?: string;
|
||||
VehicleNumber?: string;
|
||||
Amount?: number | string;
|
||||
IsExtension?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** GET /api/ParkingSession. Envelope CONFIRMED; element shape UNCONFIRMED (no active sessions in test account). */
|
||||
export interface ActiveSessionsResponse {
|
||||
ParkingSession?: ActiveSession[] | null;
|
||||
CurrentCustomerTime?: string | null;
|
||||
Response?: ResponseEnvelope;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** UNCONFIRMED element shape (no past sessions on the test account). */
|
||||
export interface PastSession {
|
||||
TransactionID?: number | string;
|
||||
ZoneName?: string;
|
||||
StartTime?: string;
|
||||
EndTime?: string;
|
||||
Amount?: number | string;
|
||||
PaymentType?: string;
|
||||
PaymentDisplay?: string;
|
||||
VehiclePlate?: string;
|
||||
IsPaid?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** GET /api/Session. Envelope CONFIRMED; element shape UNCONFIRMED (no past sessions in test account). */
|
||||
export interface PastSessionsResponse {
|
||||
Session?: PastSession[] | null;
|
||||
Response?: ResponseEnvelope;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PastSessionsParams {
|
||||
currentPage?: number | string;
|
||||
pageSize?: number | string;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Receipts */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export interface ParkingReceipt {
|
||||
TransactionID?: number | string;
|
||||
ZoneName?: string;
|
||||
StartTime?: string;
|
||||
EndTime?: string;
|
||||
Amount?: number | string;
|
||||
Total?: number | string;
|
||||
TotalCost?: number | string;
|
||||
TransactionFee?: number | string;
|
||||
PaymentType?: string;
|
||||
VehiclePlate?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** UNCONFIRMED (no receipts on the test account) — field names from static analysis. */
|
||||
export interface ReceiptResponse {
|
||||
ParkingReceipt?: ParkingReceipt;
|
||||
Response?: ResponseEnvelope;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Notification settings */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** One toggle group within notification settings. */
|
||||
export interface NotificationSetting {
|
||||
messagesEnabled?: boolean;
|
||||
emailsEnabled?: boolean;
|
||||
messages?: boolean;
|
||||
emails?: boolean;
|
||||
}
|
||||
|
||||
/** Client-side view of the settings (server sends a numeric bitmask in `Settings`). */
|
||||
export interface NotificationSettings {
|
||||
parkingReceipt?: NotificationSetting;
|
||||
overnight?: NotificationSetting;
|
||||
tenMin?: NotificationSetting;
|
||||
fifteenMin?: NotificationSetting;
|
||||
sessionEnded?: NotificationSetting;
|
||||
sessionStart?: NotificationSetting;
|
||||
}
|
||||
|
||||
/** GET /api/Setting. CONFIRMED: `{ Response, Settings }`. */
|
||||
export interface RawSettingsResponse {
|
||||
/** Server bitmask as a numeric string. Decode per the app's expandSettings logic. */
|
||||
Settings?: string;
|
||||
Response?: ResponseEnvelope;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** GET /api/State?ID=…. CONFIRMED wrapper `{ StatesResult }` (requires an ID; 404s without). */
|
||||
export interface StatesResponse {
|
||||
StatesResult?: StateItem[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Content / misc */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** Terms/Privacy/About all return `{ Value: string }`. */
|
||||
export interface ValueResponse {
|
||||
Value?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface FAQItem {
|
||||
Question?: string;
|
||||
Answer?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface FAQResponse {
|
||||
FAQs?: FAQItem[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** GET /api/ApplicationValidity — feature flags + maintenance + session bootstrap. */
|
||||
export interface ApplicationValidityResponse {
|
||||
SessionId?: string;
|
||||
RecommendUpgrade?: boolean;
|
||||
ForceUpgrade?: boolean;
|
||||
IsParkingEstimateMultiEnabled?: boolean;
|
||||
Config?: {
|
||||
IsInMaintenanceMode?: boolean;
|
||||
ExtendedVariablePBPZoneIds?: unknown;
|
||||
ExtendedVariableRateMaxDayMinutes?: number;
|
||||
ExtendedVariableRateMaxDay?: number;
|
||||
ExtendedVariableMultiDayValueThreshold?: number;
|
||||
ExtendedVariableRateSingleHourRate?: number;
|
||||
ExtendedVariableRateMaxDayJump?: number;
|
||||
ExtendedVariableRateMaxDayValueDollar?: number;
|
||||
ExtendedVariableRateMaxMode?: unknown;
|
||||
UseLimitedMetersFetch?: boolean;
|
||||
UseLoadMetersButton?: boolean;
|
||||
UseReloadMetersOnResume?: boolean;
|
||||
UseApplePay?: boolean;
|
||||
UseGooglePay?: boolean;
|
||||
ApplePayHostAddress?: string;
|
||||
UseSearchBySpace?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface StateItem {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue