@navegarti/rn-design-system 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/module/api/errors.js +105 -18
- package/lib/module/api/index.js +15 -15
- package/lib/module/api/nitro-adapter.js +290 -0
- package/lib/module/api/retry-strategy.js +42 -25
- package/lib/module/api/stores/auth-store.js +17 -3
- package/lib/module/api/types.js +0 -2
- package/lib/module/api.js +1 -1
- package/lib/module/components/Carousel/components/see-all-button.js +2 -1
- package/lib/module/components/OTPInput/index.js +24 -10
- package/lib/module/index.js +1 -1
- package/lib/typescript/src/api/errors.d.ts +98 -15
- package/lib/typescript/src/api/index.d.ts +13 -12
- package/lib/typescript/src/api/nitro-adapter.d.ts +121 -0
- package/lib/typescript/src/api/retry-strategy.d.ts +28 -9
- package/lib/typescript/src/api/stores/auth-store.d.ts +26 -3
- package/lib/typescript/src/api/types.d.ts +54 -20
- package/lib/typescript/src/api.d.ts +2 -2
- package/lib/typescript/src/components/Card/index.d.ts +9 -9
- package/lib/typescript/src/components/Card/styles.d.ts +9 -9
- package/lib/typescript/src/components/Carousel/components/see-all-button.d.ts +2 -1
- package/lib/typescript/src/components/Carousel/index.d.ts +2 -1
- package/lib/typescript/src/components/OTPInput/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/package.json +30 -29
- package/src/api/errors.ts +99 -18
- package/src/api/index.ts +15 -15
- package/src/api/nitro-adapter.ts +357 -0
- package/src/api/retry-strategy.ts +45 -26
- package/src/api/stores/auth-store.ts +26 -3
- package/src/api/types.ts +61 -21
- package/src/api.tsx +2 -2
- package/src/components/Carousel/components/see-all-button.tsx +3 -1
- package/src/components/Carousel/index.tsx +1 -1
- package/src/components/OTPInput/index.tsx +15 -1
- package/src/index.tsx +2 -2
- package/lib/module/api/axios-adapter.js +0 -154
- package/lib/typescript/src/api/axios-adapter.d.ts +0 -57
- package/src/api/axios-adapter.ts +0 -239
package/src/api/errors.ts
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Base class for all API errors
|
|
3
|
-
* Provides common structure and debugging information
|
|
2
|
+
* Base class for all API errors.
|
|
3
|
+
* Provides common structure and debugging information for HTTP-layer failures.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* try {
|
|
8
|
+
* await api.get('/users/me');
|
|
9
|
+
* } catch (error) {
|
|
10
|
+
* if (error instanceof ApiError) {
|
|
11
|
+
* console.error(error.toDebugString());
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
4
15
|
*/
|
|
5
16
|
export class ApiError extends Error {
|
|
17
|
+
/** HTTP status code associated with the error (0 for network-layer failures). */
|
|
6
18
|
public readonly statusCode: number;
|
|
19
|
+
/** URL of the request that produced this error. */
|
|
7
20
|
public readonly url?: string;
|
|
21
|
+
/** HTTP method of the request that produced this error. */
|
|
8
22
|
public readonly method?: string;
|
|
23
|
+
/** Timestamp at which the error was created. */
|
|
9
24
|
public readonly timestamp: Date;
|
|
10
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @param message Human-readable error description.
|
|
28
|
+
* @param statusCode HTTP status code (0 for network-level errors).
|
|
29
|
+
* @param url Request URL, if available.
|
|
30
|
+
* @param method HTTP method, if available.
|
|
31
|
+
*/
|
|
11
32
|
constructor(
|
|
12
33
|
message: string,
|
|
13
34
|
statusCode: number,
|
|
@@ -22,13 +43,22 @@ export class ApiError extends Error {
|
|
|
22
43
|
this.timestamp = new Date();
|
|
23
44
|
|
|
24
45
|
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
25
|
-
|
|
26
|
-
Error.captureStackTrace(this, this.constructor);
|
|
27
|
-
}
|
|
46
|
+
(Error as any).captureStackTrace?.(this, this.constructor);
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
/**
|
|
31
|
-
* Returns a formatted error
|
|
50
|
+
* Returns a formatted multi-line string with all error details, useful for
|
|
51
|
+
* logging and debugging.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* console.error(error.toDebugString());
|
|
56
|
+
* // [AuthError] Unauthorized
|
|
57
|
+
* // Status: 401
|
|
58
|
+
* // URL: /users/me
|
|
59
|
+
* // Method: GET
|
|
60
|
+
* // Time: 2026-05-06T12:00:00.000Z
|
|
61
|
+
* ```
|
|
32
62
|
*/
|
|
33
63
|
toDebugString(): string {
|
|
34
64
|
return `[${this.name}] ${this.message}\nStatus: ${this.statusCode}\nURL: ${this.url}\nMethod: ${this.method}\nTime: ${this.timestamp.toISOString()}`;
|
|
@@ -36,28 +66,49 @@ export class ApiError extends Error {
|
|
|
36
66
|
}
|
|
37
67
|
|
|
38
68
|
/**
|
|
39
|
-
* Network-
|
|
40
|
-
* HTTP Status: N/A (
|
|
69
|
+
* Network-layer errors: no internet connection, DNS failure, or connection refused.
|
|
70
|
+
* HTTP Status: N/A (statusCode is always 0).
|
|
41
71
|
*/
|
|
42
72
|
export class NetworkError extends ApiError {
|
|
73
|
+
/**
|
|
74
|
+
* @param message Human-readable description of the network failure.
|
|
75
|
+
* @param url Request URL, if available.
|
|
76
|
+
* @param method HTTP method, if available.
|
|
77
|
+
*/
|
|
43
78
|
constructor(message: string, url?: string, method?: string) {
|
|
44
79
|
super(message, 0, url, method);
|
|
45
80
|
}
|
|
46
81
|
}
|
|
47
82
|
|
|
48
83
|
/**
|
|
49
|
-
* Authentication errors (401, 403)
|
|
50
|
-
*
|
|
84
|
+
* Authentication or authorisation errors (401, 403).
|
|
85
|
+
* The client either lacks valid credentials or does not have permission.
|
|
86
|
+
*
|
|
87
|
+
* On a **401**, the {@link NitroAdapter} automatically clears the stored
|
|
88
|
+
* auth token before throwing this error.
|
|
89
|
+
*
|
|
90
|
+
* @param message Human-readable description.
|
|
91
|
+
* @param statusCode 401 or 403.
|
|
92
|
+
* @param url Request URL, if available.
|
|
93
|
+
* @param method HTTP method, if available.
|
|
51
94
|
*/
|
|
52
95
|
export class AuthError extends ApiError {}
|
|
53
96
|
|
|
54
97
|
/**
|
|
55
|
-
* Client validation errors (400, 422)
|
|
56
|
-
*
|
|
98
|
+
* Client validation errors (400, 422).
|
|
99
|
+
* The request was malformed or failed server-side validation.
|
|
57
100
|
*/
|
|
58
101
|
export class ValidationError extends ApiError {
|
|
102
|
+
/** Field-level validation error messages keyed by field name. */
|
|
59
103
|
public readonly validationErrors?: Record<string, string[]>;
|
|
60
104
|
|
|
105
|
+
/**
|
|
106
|
+
* @param message Human-readable summary.
|
|
107
|
+
* @param statusCode 400 or 422.
|
|
108
|
+
* @param url Request URL, if available.
|
|
109
|
+
* @param method HTTP method, if available.
|
|
110
|
+
* @param validationErrors Optional map of field names to their error messages.
|
|
111
|
+
*/
|
|
61
112
|
constructor(
|
|
62
113
|
message: string,
|
|
63
114
|
statusCode: number,
|
|
@@ -71,36 +122,66 @@ export class ValidationError extends ApiError {
|
|
|
71
122
|
}
|
|
72
123
|
|
|
73
124
|
/**
|
|
74
|
-
* Server errors (500, 502, 503, 504)
|
|
75
|
-
* Something went wrong on the server
|
|
125
|
+
* Server errors (500, 502, 503, 504).
|
|
126
|
+
* Something went wrong on the server side.
|
|
127
|
+
*
|
|
128
|
+
* @param message Human-readable description.
|
|
129
|
+
* @param statusCode 5xx status code.
|
|
130
|
+
* @param url Request URL, if available.
|
|
131
|
+
* @param method HTTP method, if available.
|
|
76
132
|
*/
|
|
77
133
|
export class ServerError extends ApiError {}
|
|
78
134
|
|
|
79
135
|
/**
|
|
80
|
-
* Request timeout errors
|
|
81
|
-
*
|
|
136
|
+
* Request timeout errors.
|
|
137
|
+
* The server did not respond within the configured timeout window.
|
|
138
|
+
* HTTP Status: 408.
|
|
82
139
|
*/
|
|
83
140
|
export class TimeoutError extends ApiError {
|
|
141
|
+
/**
|
|
142
|
+
* @param message Human-readable description.
|
|
143
|
+
* @param url Request URL, if available.
|
|
144
|
+
* @param method HTTP method, if available.
|
|
145
|
+
*/
|
|
84
146
|
constructor(message: string, url?: string, method?: string) {
|
|
85
147
|
super(message, 408, url, method);
|
|
86
148
|
}
|
|
87
149
|
}
|
|
88
150
|
|
|
89
151
|
/**
|
|
90
|
-
* Resource not found errors
|
|
152
|
+
* Resource not found errors.
|
|
153
|
+
* HTTP Status: 404.
|
|
91
154
|
*/
|
|
92
155
|
export class NotFoundError extends ApiError {
|
|
156
|
+
/**
|
|
157
|
+
* @param message Human-readable description.
|
|
158
|
+
* @param url Request URL, if available.
|
|
159
|
+
* @param method HTTP method, if available.
|
|
160
|
+
*/
|
|
93
161
|
constructor(message: string, url?: string, method?: string) {
|
|
94
162
|
super(message, 404, url, method);
|
|
95
163
|
}
|
|
96
164
|
}
|
|
97
165
|
|
|
98
166
|
/**
|
|
99
|
-
* Rate limit errors
|
|
167
|
+
* Rate limit errors.
|
|
168
|
+
* The server has rejected the request because too many have been sent.
|
|
169
|
+
* HTTP Status: 429.
|
|
100
170
|
*/
|
|
101
171
|
export class RateLimitError extends ApiError {
|
|
172
|
+
/**
|
|
173
|
+
* Number of seconds to wait before retrying, as advertised by the server
|
|
174
|
+
* in the `Retry-After` response header. May be `undefined` if the header
|
|
175
|
+
* was absent.
|
|
176
|
+
*/
|
|
102
177
|
public readonly retryAfter?: number;
|
|
103
178
|
|
|
179
|
+
/**
|
|
180
|
+
* @param message Human-readable description.
|
|
181
|
+
* @param url Request URL, if available.
|
|
182
|
+
* @param method HTTP method, if available.
|
|
183
|
+
* @param retryAfter Seconds until the rate limit resets, if provided by the server.
|
|
184
|
+
*/
|
|
104
185
|
constructor(
|
|
105
186
|
message: string,
|
|
106
187
|
url?: string,
|
package/src/api/index.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NitroAdapter } from './nitro-adapter';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* Pre-configured with default settings
|
|
4
|
+
* Pre-configured default adapter instance.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* import { apiRequest } from '@navegarti/rn-design-system/api';
|
|
6
|
+
* The `baseURL` placeholder is intentionally generic — consuming applications
|
|
7
|
+
* should create their own instance (or override it) with their actual API URL:
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { NitroAdapter } from '@navegarti/rn-design-system/api';
|
|
11
|
+
*
|
|
12
|
+
* export const api = new NitroAdapter({
|
|
13
|
+
* baseURL: 'https://api.myapp.com',
|
|
14
|
+
* timeout: 10_000,
|
|
15
|
+
* });
|
|
15
16
|
* ```
|
|
16
17
|
*/
|
|
17
|
-
export const apiRequest = new
|
|
18
|
-
baseURL: 'https://api.example.com', //
|
|
19
|
-
timeout:
|
|
18
|
+
export const apiRequest = new NitroAdapter({
|
|
19
|
+
baseURL: 'https://api.example.com', // Override in your application
|
|
20
|
+
timeout: 30_000,
|
|
20
21
|
retryAttempts: 3,
|
|
21
22
|
});
|
|
22
23
|
|
|
23
|
-
export { AxiosAdapter } from './axios-adapter';
|
|
24
24
|
export * from './errors';
|
|
25
|
+
export { NitroAdapter } from './nitro-adapter';
|
|
25
26
|
export { useAuthStore } from './stores/auth-store';
|
|
26
|
-
// Re-export types and classes for consumer use
|
|
27
27
|
export * from './types';
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { fetch } from 'react-native-nitro-fetch';
|
|
2
|
+
import {
|
|
3
|
+
AuthError,
|
|
4
|
+
NetworkError,
|
|
5
|
+
NotFoundError,
|
|
6
|
+
RateLimitError,
|
|
7
|
+
ServerError,
|
|
8
|
+
TimeoutError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
} from './errors';
|
|
11
|
+
import { executeWithRetry } from './retry-strategy';
|
|
12
|
+
import { useAuthStore } from './stores/auth-store';
|
|
13
|
+
import type {
|
|
14
|
+
ApiConfig,
|
|
15
|
+
IHttpAdapter,
|
|
16
|
+
RequestConfig,
|
|
17
|
+
RetryConfig,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* HTTP adapter powered by react-native-nitro-fetch.
|
|
22
|
+
*
|
|
23
|
+
* Features:
|
|
24
|
+
* - Automatic `Authorization: Bearer` token injection from the Zustand auth store.
|
|
25
|
+
* - Exponential back-off retry for transient failures (408, 429, 5xx).
|
|
26
|
+
* - AbortController-based request timeout with configurable duration.
|
|
27
|
+
* - Automatic 401 logout — clears the auth token so store subscribers can redirect.
|
|
28
|
+
* - Typed error hierarchy for precise error handling at call sites.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const api = new NitroAdapter({ baseURL: 'https://api.example.com' });
|
|
33
|
+
* api.addToken(accessToken);
|
|
34
|
+
*
|
|
35
|
+
* const user = await api.get<User>('/users/me');
|
|
36
|
+
* const post = await api.post<Post>('/posts', { title: 'Hello world' });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class NitroAdapter implements IHttpAdapter {
|
|
40
|
+
private readonly baseURL: string;
|
|
41
|
+
private readonly timeout: number;
|
|
42
|
+
private readonly defaultHeaders: Record<string, string>;
|
|
43
|
+
private readonly retryConfig: RetryConfig;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param config Adapter configuration: base URL, timeout, retry attempts,
|
|
47
|
+
* and optional default headers merged into every request.
|
|
48
|
+
*/
|
|
49
|
+
constructor(config: ApiConfig) {
|
|
50
|
+
this.baseURL = config.baseURL;
|
|
51
|
+
this.timeout = config.timeout ?? 30_000;
|
|
52
|
+
this.defaultHeaders = {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
...config.headers,
|
|
55
|
+
};
|
|
56
|
+
this.retryConfig = {
|
|
57
|
+
maxAttempts: config.retryAttempts ?? 3,
|
|
58
|
+
baseDelay: 1_000,
|
|
59
|
+
maxDelay: 10_000,
|
|
60
|
+
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolves a path or a full URL.
|
|
66
|
+
* Full URLs (starting with `http`) are used as-is; relative paths are
|
|
67
|
+
* prefixed with `baseURL`.
|
|
68
|
+
*/
|
|
69
|
+
private resolveUrl(url: string): string {
|
|
70
|
+
return url.startsWith('http') ? url : `${this.baseURL}${url}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Builds the merged headers object for a single request.
|
|
75
|
+
* Merge priority (lowest → highest): defaults → auth token → per-request overrides.
|
|
76
|
+
* The token is read fresh from the auth store on every call.
|
|
77
|
+
*/
|
|
78
|
+
private buildHeaders(extra?: Record<string, string>): Record<string, string> {
|
|
79
|
+
const token = useAuthStore.getState().token;
|
|
80
|
+
const auth: Record<string, string> = token
|
|
81
|
+
? { Authorization: `Bearer ${token}` }
|
|
82
|
+
: {};
|
|
83
|
+
return { ...this.defaultHeaders, ...auth, ...extra };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Wraps the nitro-fetch `fetch` call with an AbortController-based timeout.
|
|
88
|
+
*
|
|
89
|
+
* - Throws {@link TimeoutError} when **our** timeout fires.
|
|
90
|
+
* - Propagates a user-provided `AbortSignal` so both explicit cancellation
|
|
91
|
+
* and timeout work simultaneously.
|
|
92
|
+
* - Re-throws any other error untouched.
|
|
93
|
+
*/
|
|
94
|
+
private async performFetch(
|
|
95
|
+
fullUrl: string,
|
|
96
|
+
init: RequestInit,
|
|
97
|
+
url: string,
|
|
98
|
+
method: string,
|
|
99
|
+
): Promise<Response> {
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
let didTimeout = false;
|
|
102
|
+
|
|
103
|
+
const timer = setTimeout(() => {
|
|
104
|
+
didTimeout = true;
|
|
105
|
+
controller.abort();
|
|
106
|
+
}, this.timeout);
|
|
107
|
+
|
|
108
|
+
// Propagate user's cancel signal so an explicit abort() also cancels the request.
|
|
109
|
+
const userSignal = init.signal;
|
|
110
|
+
if (userSignal) {
|
|
111
|
+
userSignal.addEventListener('abort', () => controller.abort(), {
|
|
112
|
+
once: true,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// Pass our controller's signal; nitro-fetch honours the standard AbortSignal.
|
|
118
|
+
return await fetch(fullUrl, {
|
|
119
|
+
...init,
|
|
120
|
+
signal: controller.signal,
|
|
121
|
+
} as Parameters<typeof fetch>[1]);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (didTimeout) {
|
|
124
|
+
throw new TimeoutError(
|
|
125
|
+
'Request timeout - server took too long to respond',
|
|
126
|
+
url,
|
|
127
|
+
method,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
} finally {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reads a failed response body and throws the appropriate typed error.
|
|
138
|
+
*
|
|
139
|
+
* On **401 Unauthorized**, the auth token is cleared before throwing
|
|
140
|
+
* {@link AuthError} so that any Zustand subscriber watching `token` can
|
|
141
|
+
* redirect the user to the login screen automatically.
|
|
142
|
+
*
|
|
143
|
+
* @returns This method always throws — it never resolves.
|
|
144
|
+
*/
|
|
145
|
+
private async parseErrorResponse(
|
|
146
|
+
response: Response,
|
|
147
|
+
url: string,
|
|
148
|
+
method: string,
|
|
149
|
+
): Promise<never> {
|
|
150
|
+
let message = 'An error occurred';
|
|
151
|
+
let validationErrors: Record<string, string[]> | undefined;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const body = (await response.json()) as {
|
|
155
|
+
message?: string;
|
|
156
|
+
errors?: Record<string, string[]>;
|
|
157
|
+
};
|
|
158
|
+
message = body.message ?? message;
|
|
159
|
+
validationErrors = body.errors;
|
|
160
|
+
} catch {
|
|
161
|
+
message = response.statusText || message;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { status } = response;
|
|
165
|
+
|
|
166
|
+
if (status === 401) {
|
|
167
|
+
// Auto-logout: clear token so store subscribers can redirect.
|
|
168
|
+
useAuthStore.getState().clearToken();
|
|
169
|
+
throw new AuthError(message, status, url, method);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (status === 403) {
|
|
173
|
+
throw new AuthError(message, status, url, method);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (status === 404) {
|
|
177
|
+
throw new NotFoundError(message, url, method);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (status === 429) {
|
|
181
|
+
const retryAfter =
|
|
182
|
+
Number(response.headers.get('retry-after')) || undefined;
|
|
183
|
+
throw new RateLimitError(message, url, method, retryAfter);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (status === 400 || status === 422) {
|
|
187
|
+
throw new ValidationError(message, status, url, method, validationErrors);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (status >= 500) {
|
|
191
|
+
throw new ServerError(message, status, url, method);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw new NetworkError(message, url, method);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Core request runner: resolves the URL, builds headers, executes the
|
|
199
|
+
* request with retry logic, and parses the response into `T`.
|
|
200
|
+
*
|
|
201
|
+
* A user-initiated `AbortError` (signal not caused by our timeout) is
|
|
202
|
+
* surfaced as a {@link NetworkError} with the message "Request was cancelled".
|
|
203
|
+
*/
|
|
204
|
+
private async executeRequest<T>(
|
|
205
|
+
url: string,
|
|
206
|
+
method: string,
|
|
207
|
+
init: { method: string; body?: string | null },
|
|
208
|
+
config?: RequestConfig,
|
|
209
|
+
): Promise<T> {
|
|
210
|
+
const fullUrl = this.resolveUrl(url);
|
|
211
|
+
const headers = this.buildHeaders(config?.headers);
|
|
212
|
+
|
|
213
|
+
const mergedInit: RequestInit = { ...init, headers };
|
|
214
|
+
|
|
215
|
+
if (config?.signal !== undefined) {
|
|
216
|
+
mergedInit.signal = config.signal;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// `cache` and `credentials` are not in RN's RequestInit declaration but are
|
|
220
|
+
// accepted by react-native-nitro-fetch at runtime.
|
|
221
|
+
const extendedInit = mergedInit as Record<string, unknown>;
|
|
222
|
+
if (config?.cache !== undefined) {
|
|
223
|
+
extendedInit.cache = config.cache;
|
|
224
|
+
}
|
|
225
|
+
if (config?.credentials !== undefined) {
|
|
226
|
+
extendedInit.credentials = config.credentials;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let response: Response;
|
|
230
|
+
try {
|
|
231
|
+
response = await executeWithRetry(
|
|
232
|
+
() => this.performFetch(fullUrl, mergedInit, url, method),
|
|
233
|
+
this.retryConfig,
|
|
234
|
+
(attempt, delay, error) => {
|
|
235
|
+
console.log(
|
|
236
|
+
`[API Retry] Attempt ${attempt} failed, retrying in ${delay}ms…`,
|
|
237
|
+
error.message,
|
|
238
|
+
);
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// User-initiated cancellation (our timeout converts its AbortError to TimeoutError,
|
|
243
|
+
// so any remaining AbortError here came from the caller's own signal).
|
|
244
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
245
|
+
throw new NetworkError('Request was cancelled', url, method);
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
return this.parseErrorResponse(response, url, method);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 204 No Content or non-JSON response — return undefined typed as T.
|
|
255
|
+
const contentType = response.headers.get('content-type');
|
|
256
|
+
if (response.status === 204 || !contentType?.includes('application/json')) {
|
|
257
|
+
return undefined as T;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return response.json() as Promise<T>;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Performs a GET request.
|
|
265
|
+
*
|
|
266
|
+
* @template T Expected response body type.
|
|
267
|
+
* @param url Request path or full URL.
|
|
268
|
+
* @param config Optional per-request overrides (headers, signal, cache, credentials).
|
|
269
|
+
*/
|
|
270
|
+
async get<T = unknown>(url: string, config?: RequestConfig): Promise<T> {
|
|
271
|
+
return this.executeRequest<T>(url, 'GET', { method: 'GET' }, config);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Performs a POST request.
|
|
276
|
+
*
|
|
277
|
+
* @template T Expected response body type.
|
|
278
|
+
* @param url Request path or full URL.
|
|
279
|
+
* @param data Request body — will be JSON-serialised if provided.
|
|
280
|
+
* @param config Optional per-request overrides.
|
|
281
|
+
*/
|
|
282
|
+
async post<T = unknown>(
|
|
283
|
+
url: string,
|
|
284
|
+
data?: unknown,
|
|
285
|
+
config?: RequestConfig,
|
|
286
|
+
): Promise<T> {
|
|
287
|
+
return this.executeRequest<T>(
|
|
288
|
+
url,
|
|
289
|
+
'POST',
|
|
290
|
+
{
|
|
291
|
+
method: 'POST',
|
|
292
|
+
body: data !== undefined ? JSON.stringify(data) : null,
|
|
293
|
+
},
|
|
294
|
+
config,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Performs a PUT request.
|
|
300
|
+
*
|
|
301
|
+
* @template T Expected response body type.
|
|
302
|
+
* @param url Request path or full URL.
|
|
303
|
+
* @param data Request body — will be JSON-serialised if provided.
|
|
304
|
+
* @param config Optional per-request overrides.
|
|
305
|
+
*/
|
|
306
|
+
async put<T = unknown>(
|
|
307
|
+
url: string,
|
|
308
|
+
data?: unknown,
|
|
309
|
+
config?: RequestConfig,
|
|
310
|
+
): Promise<T> {
|
|
311
|
+
return this.executeRequest<T>(
|
|
312
|
+
url,
|
|
313
|
+
'PUT',
|
|
314
|
+
{
|
|
315
|
+
method: 'PUT',
|
|
316
|
+
body: data !== undefined ? JSON.stringify(data) : null,
|
|
317
|
+
},
|
|
318
|
+
config,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Performs a DELETE request.
|
|
324
|
+
*
|
|
325
|
+
* @template T Expected response body type.
|
|
326
|
+
* @param url Request path or full URL.
|
|
327
|
+
* @param config Optional per-request overrides.
|
|
328
|
+
*/
|
|
329
|
+
async delete<T = unknown>(url: string, config?: RequestConfig): Promise<T> {
|
|
330
|
+
return this.executeRequest<T>(url, 'DELETE', { method: 'DELETE' }, config);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Stores an auth token and attaches it as `Authorization: Bearer <token>`
|
|
335
|
+
* on all subsequent requests.
|
|
336
|
+
*
|
|
337
|
+
* @param token Bearer token value.
|
|
338
|
+
*/
|
|
339
|
+
addToken(token: string): void {
|
|
340
|
+
useAuthStore.getState().setToken(token);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Clears the stored auth token, preventing it from being sent on
|
|
345
|
+
* subsequent requests.
|
|
346
|
+
*/
|
|
347
|
+
removeToken(): void {
|
|
348
|
+
useAuthStore.getState().clearToken();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Returns the currently stored auth token, or `null` if none is set.
|
|
353
|
+
*/
|
|
354
|
+
getToken(): string | null {
|
|
355
|
+
return useAuthStore.getState().token;
|
|
356
|
+
}
|
|
357
|
+
}
|