- Client: server returns 200 + Status:"Error" on bad credentials; loginWith* now throw LoginError with the server message instead of faking a signed-in state - Map: uncontrolled camera positioned once on load + explicit actions only, so Search/marker-tap/re-renders no longer snap back to the user's location - Markers: always-dark high-contrast bubble + zone-colored dot (some zones report a white BackgroundColor -> was white-on-white) - Dark mode now switches map tiles to CARTO dark-matter Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
748 lines
24 KiB
TypeScript
748 lines
24 KiB
TypeScript
/**
|
|
* 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;
|
|
/** Called when any authenticated request returns 401 (token cleared automatically). */
|
|
onUnauthorized?: () => void;
|
|
/** Log every request/response (redacted) — useful for on-device debugging via logcat. */
|
|
logRequests?: boolean;
|
|
/** Where log lines go (default console.log). */
|
|
logSink?: (line: string) => void;
|
|
}
|
|
|
|
/** Thrown when credentials are rejected (the server signals this via a 200 + Status:"Error"). */
|
|
export class LoginError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = 'LoginError';
|
|
}
|
|
}
|
|
|
|
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,
|
|
onUnauthorized: options.onUnauthorized,
|
|
logRequests: options.logRequests,
|
|
logSink: options.logSink,
|
|
});
|
|
}
|
|
|
|
/** Set the locale used for the `localeCode` query param on subsequent calls. */
|
|
setLocale(localeCode: string): void {
|
|
this.http.setLocaleCode(localeCode);
|
|
}
|
|
|
|
/** Toggle redacted request/response logging at runtime. */
|
|
setLogRequests(on: boolean): void {
|
|
this.http.setLogRequests(on);
|
|
}
|
|
|
|
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 */
|
|
/* ============================================================== */
|
|
|
|
/**
|
|
* The server returns HTTP 200 even for a FAILED login, signalling the failure
|
|
* only via `Status: "Error"` + a null `Auth_Token`. Detect that here so callers
|
|
* get a real error instead of a phantom "signed-in" state.
|
|
*/
|
|
private finishLogin(data: T.AuthResponse): Promise<T.AuthResponse> {
|
|
const token = typeof data?.Auth_Token === 'string' ? data.Auth_Token : '';
|
|
if (!token || data?.Status === 'Error') {
|
|
throw new LoginError(data?.Message || 'Invalid login or password.');
|
|
}
|
|
return this.tokens.setAuthToken
|
|
? Promise.resolve(this.tokens.setAuthToken(token)).then(() => data)
|
|
: Promise.resolve(data);
|
|
}
|
|
|
|
/** 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,
|
|
});
|
|
return this.finishLogin(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,
|
|
});
|
|
return this.finishLogin(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,
|
|
}),
|
|
);
|
|
}
|
|
}
|