Add request logging mode, dark mode, Account hub, profile, vehicle CRUD, cards view
- Client: logRequests option + redacted request/response logging (setLogRequests to toggle at runtime); enabled via app extra.debugHttp for on-device 401 debugging - Dark mode (persisted) via ThemeProvider + React Navigation theme - New "Account" tab: Profile (view), Vehicles (full CRUD), Payment methods (view-only), About, dark-mode + request-logging toggles, sign out Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
65e9118806
commit
2f9f0f604a
11 changed files with 707 additions and 8 deletions
|
|
@ -45,6 +45,10 @@ export interface ParkSmarterClientOptions {
|
|||
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;
|
||||
}
|
||||
|
||||
function resolveEnvironment(
|
||||
|
|
@ -69,6 +73,8 @@ export class ParkSmarterClient {
|
|||
fetchImpl: options.fetchImpl,
|
||||
uuid: options.uuid,
|
||||
onUnauthorized: options.onUnauthorized,
|
||||
logRequests: options.logRequests,
|
||||
logSink: options.logSink,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +83,11 @@ export class ParkSmarterClient {
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,49 @@ export interface HttpClientConfig {
|
|||
* stored token and route the user back to sign-in. Fired before the error throws.
|
||||
*/
|
||||
onUnauthorized?: () => void;
|
||||
/**
|
||||
* When true, logs every request/response (method, URL, headers, body, status)
|
||||
* via `logSink` (default: console.log). Sensitive header/body values are
|
||||
* redacted. Handy for capturing traffic in logcat while debugging; turn off in
|
||||
* production.
|
||||
*/
|
||||
logRequests?: boolean;
|
||||
/** Where log lines go when logRequests is on. Defaults to console.log. */
|
||||
logSink?: (line: string) => void;
|
||||
}
|
||||
|
||||
/** Header/body keys whose VALUES must never be logged in the clear. */
|
||||
const SENSITIVE_KEYS = new Set(
|
||||
[
|
||||
'Auth_Token',
|
||||
'Application_Token',
|
||||
'ParkSmarter_SessionId',
|
||||
'Password',
|
||||
'OldPassword',
|
||||
'NewPassword',
|
||||
'CCNumber',
|
||||
'CCExpDate',
|
||||
'CCZip',
|
||||
'CCFirstSix',
|
||||
'CCLastFour',
|
||||
'ProviderAuth',
|
||||
'ProviderIdentity',
|
||||
].map((k) => k.toLowerCase()),
|
||||
);
|
||||
|
||||
function redact(obj: unknown): unknown {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
||||
if (SENSITIVE_KEYS.has(k.toLowerCase())) {
|
||||
out[k] = typeof v === 'string' ? `<redacted:${v.length}>` : '<redacted>';
|
||||
} else if (v && typeof v === 'object') {
|
||||
out[k] = redact(v);
|
||||
} else {
|
||||
out[k] = v;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function defaultUuid(): string {
|
||||
|
|
@ -141,6 +184,8 @@ export class HttpClient {
|
|||
fetchImpl: config.fetchImpl ?? globalThis.fetch?.bind(globalThis),
|
||||
uuid: config.uuid ?? defaultUuid,
|
||||
onUnauthorized: config.onUnauthorized ?? (() => {}),
|
||||
logRequests: config.logRequests ?? false,
|
||||
logSink: config.logSink ?? ((line: string) => console.log(line)),
|
||||
};
|
||||
if (!this.cfg.fetchImpl) {
|
||||
throw new Error(
|
||||
|
|
@ -161,6 +206,11 @@ export class HttpClient {
|
|||
this.cfg.localeCode = localeCode;
|
||||
}
|
||||
|
||||
/** Toggle request/response logging at runtime (e.g. from a Settings switch). */
|
||||
setLogRequests(on: boolean) {
|
||||
this.cfg.logRequests = on;
|
||||
}
|
||||
|
||||
async request<T>(opts: RequestOptions): Promise<ParkSmarterResponse<T>> {
|
||||
const { method } = opts;
|
||||
const requestId = this.cfg.uuid();
|
||||
|
|
@ -202,6 +252,15 @@ export class HttpClient {
|
|||
else opts.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
||||
}
|
||||
|
||||
if (this.cfg.logRequests) {
|
||||
this.cfg.logSink(
|
||||
`[PS →] ${method} ${url}\n headers: ${JSON.stringify(redact(headers))}` +
|
||||
(opts.body !== undefined && opts.body !== null
|
||||
? `\n body: ${JSON.stringify(redact(opts.body))}`
|
||||
: ''),
|
||||
);
|
||||
}
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await this.cfg.fetchImpl(url, {
|
||||
|
|
@ -249,6 +308,14 @@ export class HttpClient {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.cfg.logRequests) {
|
||||
const preview =
|
||||
data && typeof data === 'object'
|
||||
? JSON.stringify(redact(data)).slice(0, 500)
|
||||
: String(text).slice(0, 300);
|
||||
this.cfg.logSink(`[PS ←] ${res.status} ${method} ${opts.path} ${preview}`);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
// Token invalid/rotated/expired — let the app clear it and re-auth.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue