@navegarti/rn-design-system 0.8.1 → 0.8.2
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/package.json +2 -1
- package/src/api/axios-adapter.ts +239 -0
- package/src/api/errors.ts +113 -0
- package/src/api/index.ts +27 -0
- package/src/api/retry-strategy.ts +86 -0
- package/src/api/stores/auth-store.ts +26 -0
- package/src/api/types.ts +74 -0
- package/src/api.tsx +17 -0
- package/src/components/Button/index.tsx +37 -0
- package/src/components/Button/styles.ts +28 -0
- package/src/components/Card/index.tsx +23 -0
- package/src/components/Card/styles.ts +45 -0
- package/src/components/Card/types.ts +22 -0
- package/src/components/Carousel/components/dot.tsx +43 -0
- package/src/components/Carousel/components/footer.tsx +21 -0
- package/src/components/Carousel/components/list.tsx +81 -0
- package/src/components/Carousel/components/pagination.tsx +15 -0
- package/src/components/Carousel/components/see-all-button.tsx +29 -0
- package/src/components/Carousel/context.tsx +19 -0
- package/src/components/Carousel/index.tsx +193 -0
- package/src/components/Carousel/styles.ts +8 -0
- package/src/components/Carousel/types.ts +45 -0
- package/src/components/Checkbox/index.tsx +42 -0
- package/src/components/Checkbox/styles.ts +7 -0
- package/src/components/FAB/components/extended-fab.tsx +17 -0
- package/src/components/FAB/index.tsx +24 -0
- package/src/components/FAB/styles.ts +61 -0
- package/src/components/FAB/utils.ts +45 -0
- package/src/components/Flex/index.tsx +1 -0
- package/src/components/Flex/styles.ts +47 -0
- package/src/components/FormLabel/index.tsx +6 -0
- package/src/components/FormLabel/styles.ts +8 -0
- package/src/components/Input/components/input-control.tsx +20 -0
- package/src/components/Input/components/input-error.tsx +14 -0
- package/src/components/Input/components/input-field.tsx +55 -0
- package/src/components/Input/components/input-icon.tsx +7 -0
- package/src/components/Input/components/input-password-toggle.tsx +19 -0
- package/src/components/Input/components/input-root.tsx +54 -0
- package/src/components/Input/context.tsx +26 -0
- package/src/components/Input/index.tsx +17 -0
- package/src/components/Input/styles.ts +46 -0
- package/src/components/Input/utils.ts +27 -0
- package/src/components/Margin/index.tsx +1 -0
- package/src/components/Margin/styles.ts +23 -0
- package/src/components/OTPInput/index.tsx +114 -0
- package/src/components/OTPInput/styles.ts +23 -0
- package/src/components/OTPInput/utils.ts +31 -0
- package/src/components/Padding/index.tsx +1 -0
- package/src/components/Padding/styles.ts +23 -0
- package/src/components/Radio/components/radio-item.tsx +53 -0
- package/src/components/Radio/context.tsx +10 -0
- package/src/components/Radio/index.tsx +32 -0
- package/src/components/Radio/styles.ts +38 -0
- package/src/components/Radio/utils.ts +19 -0
- package/src/components/Select/components/error-icon.tsx +3 -0
- package/src/components/Select/components/select-error.tsx +14 -0
- package/src/components/Select/components/select-field.tsx +72 -0
- package/src/components/Select/context.tsx +17 -0
- package/src/components/Select/index.tsx +24 -0
- package/src/components/Select/styles.ts +15 -0
- package/src/components/Skeleton/index.tsx +60 -0
- package/src/components/Switch/index.tsx +61 -0
- package/src/components/Switch/styles.ts +32 -0
- package/src/components/Switch/utils.ts +15 -0
- package/src/components/Text/index.tsx +2 -0
- package/src/components/Text/styles.ts +44 -0
- package/src/components.tsx +28 -0
- package/src/form.tsx +6 -0
- package/src/formValidators/ZodTypeValidator.ts +58 -0
- package/src/formValidators/index.ts +8 -0
- package/src/formValidators/types.ts +20 -0
- package/src/hooks/useAppIsActive.ts +28 -0
- package/src/hooks/useAppSecurity.tsx +35 -0
- package/src/hooks/useNetworkStatus.ts +75 -0
- package/src/hooks/useStatusBar.ts +34 -0
- package/src/hooks.tsx +9 -0
- package/src/index.tsx +76 -0
- package/src/utils/camelCase.ts +6 -0
- package/src/utils/camelCaseJSONKeys.ts +31 -0
- package/src/utils/capitalizeWord.ts +4 -0
- package/src/utils/createLinkingPhoneNumberString.ts +13 -0
- package/src/utils/dateFormatters.ts +48 -0
- package/src/utils/debounce.ts +15 -0
- package/src/utils/deepLinkParser.ts +87 -0
- package/src/utils/getAge.ts +12 -0
- package/src/utils/getOnlyNumbers.ts +1 -0
- package/src/utils/isArray.ts +4 -0
- package/src/utils/isObject.ts +4 -0
- package/src/utils/masks.ts +3 -0
- package/src/utils/priceFormatter.ts +11 -0
- package/src/utils/removeTextAccents.ts +4 -0
- package/src/utils/sortBy.ts +21 -0
- package/src/utils/uniqBy.ts +20 -0
- package/src/utils/userFullnameInitialsExtractor.ts +22 -0
- package/src/utils.tsx +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navegarti/rn-design-system",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Components and functions to help navegar projects",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
73
|
"files": [
|
|
74
|
+
"src",
|
|
74
75
|
"lib",
|
|
75
76
|
"components.js",
|
|
76
77
|
"utils.js",
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import axios, {
|
|
2
|
+
type AxiosError,
|
|
3
|
+
type AxiosInstance,
|
|
4
|
+
type AxiosRequestConfig,
|
|
5
|
+
type AxiosResponse,
|
|
6
|
+
} from 'axios';
|
|
7
|
+
import {
|
|
8
|
+
AuthError,
|
|
9
|
+
NetworkError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
ServerError,
|
|
13
|
+
TimeoutError,
|
|
14
|
+
ValidationError,
|
|
15
|
+
} from './errors';
|
|
16
|
+
import { executeWithRetry } from './retry-strategy';
|
|
17
|
+
import { useAuthStore } from './stores/auth-store';
|
|
18
|
+
import type { ApiConfig, IHttpAdapter, RetryConfig } from './types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Axios-based HTTP adapter implementation
|
|
22
|
+
* Implements retry logic, error handling, and token management
|
|
23
|
+
*/
|
|
24
|
+
export class AxiosAdapter implements IHttpAdapter {
|
|
25
|
+
private readonly axiosInstance: AxiosInstance;
|
|
26
|
+
private readonly retryConfig: RetryConfig;
|
|
27
|
+
|
|
28
|
+
constructor(config: ApiConfig) {
|
|
29
|
+
// Initialize axios instance with configuration
|
|
30
|
+
this.axiosInstance = axios.create({
|
|
31
|
+
baseURL: config.baseURL,
|
|
32
|
+
timeout: config.timeout ?? 30000, // 30 seconds default
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
...config.headers,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Configure retry behavior
|
|
40
|
+
this.retryConfig = {
|
|
41
|
+
maxAttempts: config.retryAttempts ?? 3,
|
|
42
|
+
baseDelay: 1000, // 1 second
|
|
43
|
+
maxDelay: 10000, // 10 seconds
|
|
44
|
+
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Setup request interceptor to inject auth token
|
|
48
|
+
this.setupRequestInterceptor();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Sets up request interceptor to automatically add auth token
|
|
53
|
+
*/
|
|
54
|
+
private setupRequestInterceptor(): void {
|
|
55
|
+
this.axiosInstance.interceptors.request.use(
|
|
56
|
+
(config) => {
|
|
57
|
+
const token = useAuthStore.getState().token;
|
|
58
|
+
|
|
59
|
+
if (token && config.headers) {
|
|
60
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return config;
|
|
64
|
+
},
|
|
65
|
+
(error) => Promise.reject(error),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Maps axios error to appropriate custom error type
|
|
71
|
+
*/
|
|
72
|
+
private mapError(error: AxiosError, url?: string, method?: string): Error {
|
|
73
|
+
// Network errors (no response received)
|
|
74
|
+
if (!error.response) {
|
|
75
|
+
if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
|
|
76
|
+
return new TimeoutError(
|
|
77
|
+
'Request timeout - server took too long to respond',
|
|
78
|
+
url,
|
|
79
|
+
method,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return new NetworkError(
|
|
83
|
+
error.message || 'Network error - please check your connection',
|
|
84
|
+
url,
|
|
85
|
+
method,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const statusCode = error.response.status;
|
|
90
|
+
const responseData = error.response.data as {
|
|
91
|
+
message?: string;
|
|
92
|
+
errors?: Record<string, string[]>;
|
|
93
|
+
};
|
|
94
|
+
const message =
|
|
95
|
+
responseData?.message || error.message || 'An error occurred';
|
|
96
|
+
|
|
97
|
+
// Map status codes to error types
|
|
98
|
+
switch (true) {
|
|
99
|
+
case statusCode === 401 || statusCode === 403:
|
|
100
|
+
return new AuthError(message, statusCode, url, method);
|
|
101
|
+
|
|
102
|
+
case statusCode === 404:
|
|
103
|
+
return new NotFoundError(message, url, method);
|
|
104
|
+
|
|
105
|
+
case statusCode === 429:
|
|
106
|
+
return new RateLimitError(
|
|
107
|
+
message,
|
|
108
|
+
url,
|
|
109
|
+
method,
|
|
110
|
+
Number(error.response.headers['retry-after']),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
case statusCode === 400 || statusCode === 422:
|
|
114
|
+
return new ValidationError(
|
|
115
|
+
message,
|
|
116
|
+
statusCode,
|
|
117
|
+
url,
|
|
118
|
+
method,
|
|
119
|
+
responseData?.errors,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
case statusCode >= 500:
|
|
123
|
+
return new ServerError(message, statusCode, url, method);
|
|
124
|
+
|
|
125
|
+
default:
|
|
126
|
+
return new NetworkError(message, url, method);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Transforms axios response to standardized ApiResponse format
|
|
132
|
+
*/
|
|
133
|
+
private transformResponse<T>(response: AxiosResponse<T>): T {
|
|
134
|
+
return response.data;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Executes HTTP request with retry logic and error handling
|
|
139
|
+
*/
|
|
140
|
+
private async executeRequest<T>(
|
|
141
|
+
requestFn: () => Promise<AxiosResponse<T>>,
|
|
142
|
+
url?: string,
|
|
143
|
+
method?: string,
|
|
144
|
+
): Promise<T> {
|
|
145
|
+
try {
|
|
146
|
+
const response = await executeWithRetry(
|
|
147
|
+
requestFn,
|
|
148
|
+
this.retryConfig,
|
|
149
|
+
(attempt, delay, error) => {
|
|
150
|
+
console.log(
|
|
151
|
+
`[API Retry] Attempt ${attempt} failed, retrying in ${delay}ms...`,
|
|
152
|
+
error.message,
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return this.transformResponse(response);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
throw this.mapError(error as AxiosError, url, method);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Performs GET request
|
|
165
|
+
*/
|
|
166
|
+
async get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
167
|
+
return this.executeRequest(
|
|
168
|
+
() => this.axiosInstance.get<T>(url, config),
|
|
169
|
+
url,
|
|
170
|
+
'GET',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Performs POST request
|
|
176
|
+
*/
|
|
177
|
+
async post<T = unknown>(
|
|
178
|
+
url: string,
|
|
179
|
+
data?: unknown,
|
|
180
|
+
config?: AxiosRequestConfig,
|
|
181
|
+
): Promise<T> {
|
|
182
|
+
return this.executeRequest(
|
|
183
|
+
() => this.axiosInstance.post<T>(url, data, config),
|
|
184
|
+
url,
|
|
185
|
+
'POST',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Performs PUT request
|
|
191
|
+
*/
|
|
192
|
+
async put<T = unknown>(
|
|
193
|
+
url: string,
|
|
194
|
+
data?: unknown,
|
|
195
|
+
config?: AxiosRequestConfig,
|
|
196
|
+
): Promise<T> {
|
|
197
|
+
return this.executeRequest(
|
|
198
|
+
() => this.axiosInstance.put<T>(url, data, config),
|
|
199
|
+
url,
|
|
200
|
+
'PUT',
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Performs DELETE request
|
|
206
|
+
*/
|
|
207
|
+
async delete<T = unknown>(
|
|
208
|
+
url: string,
|
|
209
|
+
config?: AxiosRequestConfig,
|
|
210
|
+
): Promise<T> {
|
|
211
|
+
return this.executeRequest(
|
|
212
|
+
() => this.axiosInstance.delete<T>(url, config),
|
|
213
|
+
url,
|
|
214
|
+
'DELETE',
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Adds authentication token to store
|
|
220
|
+
* Token will be automatically included in all subsequent requests
|
|
221
|
+
*/
|
|
222
|
+
addToken(token: string): void {
|
|
223
|
+
useAuthStore.getState().setToken(token);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Removes authentication token from store
|
|
228
|
+
*/
|
|
229
|
+
removeToken(): void {
|
|
230
|
+
useAuthStore.getState().clearToken();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get token from store
|
|
235
|
+
*/
|
|
236
|
+
getToken(): string | null {
|
|
237
|
+
return useAuthStore.getState().token;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all API errors
|
|
3
|
+
* Provides common structure and debugging information
|
|
4
|
+
*/
|
|
5
|
+
export class ApiError extends Error {
|
|
6
|
+
public readonly statusCode: number;
|
|
7
|
+
public readonly url?: string;
|
|
8
|
+
public readonly method?: string;
|
|
9
|
+
public readonly timestamp: Date;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
message: string,
|
|
13
|
+
statusCode: number,
|
|
14
|
+
url?: string,
|
|
15
|
+
method?: string,
|
|
16
|
+
) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = this.constructor.name;
|
|
19
|
+
this.statusCode = statusCode;
|
|
20
|
+
this.url = url;
|
|
21
|
+
this.method = method;
|
|
22
|
+
this.timestamp = new Date();
|
|
23
|
+
|
|
24
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
25
|
+
if (Error.captureStackTrace) {
|
|
26
|
+
Error.captureStackTrace(this, this.constructor);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns a formatted error message for debugging
|
|
32
|
+
*/
|
|
33
|
+
toDebugString(): string {
|
|
34
|
+
return `[${this.name}] ${this.message}\nStatus: ${this.statusCode}\nURL: ${this.url}\nMethod: ${this.method}\nTime: ${this.timestamp.toISOString()}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Network-related errors (no internet, DNS failure, timeout)
|
|
40
|
+
* HTTP Status: N/A (network layer)
|
|
41
|
+
*/
|
|
42
|
+
export class NetworkError extends ApiError {
|
|
43
|
+
constructor(message: string, url?: string, method?: string) {
|
|
44
|
+
super(message, 0, url, method);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Authentication errors (401, 403)
|
|
50
|
+
* Client lacks valid credentials or permissions
|
|
51
|
+
*/
|
|
52
|
+
export class AuthError extends ApiError {}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Client validation errors (400, 422)
|
|
56
|
+
* Request malformed or validation failed
|
|
57
|
+
*/
|
|
58
|
+
export class ValidationError extends ApiError {
|
|
59
|
+
public readonly validationErrors?: Record<string, string[]>;
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
message: string,
|
|
63
|
+
statusCode: number,
|
|
64
|
+
url?: string,
|
|
65
|
+
method?: string,
|
|
66
|
+
validationErrors?: Record<string, string[]>,
|
|
67
|
+
) {
|
|
68
|
+
super(message, statusCode, url, method);
|
|
69
|
+
this.validationErrors = validationErrors;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Server errors (500, 502, 503, 504)
|
|
75
|
+
* Something went wrong on the server
|
|
76
|
+
*/
|
|
77
|
+
export class ServerError extends ApiError {}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Request timeout errors
|
|
81
|
+
* Request took too long to complete
|
|
82
|
+
*/
|
|
83
|
+
export class TimeoutError extends ApiError {
|
|
84
|
+
constructor(message: string, url?: string, method?: string) {
|
|
85
|
+
super(message, 408, url, method);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resource not found errors (404)
|
|
91
|
+
*/
|
|
92
|
+
export class NotFoundError extends ApiError {
|
|
93
|
+
constructor(message: string, url?: string, method?: string) {
|
|
94
|
+
super(message, 404, url, method);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Rate limit errors (429)
|
|
100
|
+
*/
|
|
101
|
+
export class RateLimitError extends ApiError {
|
|
102
|
+
public readonly retryAfter?: number;
|
|
103
|
+
|
|
104
|
+
constructor(
|
|
105
|
+
message: string,
|
|
106
|
+
url?: string,
|
|
107
|
+
method?: string,
|
|
108
|
+
retryAfter?: number,
|
|
109
|
+
) {
|
|
110
|
+
super(message, 429, url, method);
|
|
111
|
+
this.retryAfter = retryAfter;
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AxiosAdapter } from './axios-adapter';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default API request instance
|
|
5
|
+
* Pre-configured with default settings
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { apiRequest } from '@navegarti/rn-design-system/api';
|
|
10
|
+
*
|
|
11
|
+
* const response = await apiRequest.get<User>('/users/123');
|
|
12
|
+
* if (response.success) {
|
|
13
|
+
* console.log(response.data);
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export const apiRequest = new AxiosAdapter({
|
|
18
|
+
baseURL: 'https://api.example.com', // Will be overridden by consumers
|
|
19
|
+
timeout: 30000,
|
|
20
|
+
retryAttempts: 3,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export { AxiosAdapter } from './axios-adapter';
|
|
24
|
+
export * from './errors';
|
|
25
|
+
export { useAuthStore } from './stores/auth-store';
|
|
26
|
+
// Re-export types and classes for consumer use
|
|
27
|
+
export * from './types';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { AxiosError } from 'axios';
|
|
2
|
+
import type { RetryConfig } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calculates exponential backoff delay
|
|
6
|
+
* Formula: min(baseDelay * (2 ^ attempt), maxDelay)
|
|
7
|
+
*/
|
|
8
|
+
export function calculateBackoffDelay(
|
|
9
|
+
attempt: number,
|
|
10
|
+
baseDelay: number,
|
|
11
|
+
maxDelay: number,
|
|
12
|
+
): number {
|
|
13
|
+
const exponentialDelay = baseDelay * 2 ** attempt;
|
|
14
|
+
return Math.min(exponentialDelay, maxDelay);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Determines if an error is retryable based on configuration
|
|
19
|
+
*/
|
|
20
|
+
export function isRetryableError(
|
|
21
|
+
error: AxiosError,
|
|
22
|
+
retryConfig: RetryConfig,
|
|
23
|
+
): boolean {
|
|
24
|
+
// Network errors (no response) are always retryable
|
|
25
|
+
if (!error.response) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const statusCode = error.response.status;
|
|
30
|
+
|
|
31
|
+
// Check if status code is in retryable list
|
|
32
|
+
return retryConfig.retryableStatusCodes.includes(statusCode);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Sleep utility for retry delays
|
|
37
|
+
*/
|
|
38
|
+
export function sleep(ms: number): Promise<void> {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
setTimeout(resolve, ms);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Executes a request with retry logic
|
|
46
|
+
* @template T - The expected response type
|
|
47
|
+
*/
|
|
48
|
+
export async function executeWithRetry<T>(
|
|
49
|
+
requestFn: () => Promise<T>,
|
|
50
|
+
retryConfig: RetryConfig,
|
|
51
|
+
onRetry?: (attempt: number, delay: number, error: AxiosError) => void,
|
|
52
|
+
): Promise<T> {
|
|
53
|
+
let lastError: AxiosError | undefined;
|
|
54
|
+
|
|
55
|
+
for (let attempt = 0; attempt < retryConfig.maxAttempts; attempt++) {
|
|
56
|
+
try {
|
|
57
|
+
return await requestFn();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const axiosError = error as AxiosError;
|
|
60
|
+
lastError = axiosError;
|
|
61
|
+
|
|
62
|
+
// Don't retry if not retryable or if this was the last attempt
|
|
63
|
+
const isLastAttempt = attempt === retryConfig.maxAttempts - 1;
|
|
64
|
+
if (!isRetryableError(axiosError, retryConfig) || isLastAttempt) {
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Calculate delay and wait before retrying
|
|
69
|
+
const delay = calculateBackoffDelay(
|
|
70
|
+
attempt,
|
|
71
|
+
retryConfig.baseDelay,
|
|
72
|
+
retryConfig.maxDelay,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Notify callback if provided
|
|
76
|
+
if (onRetry) {
|
|
77
|
+
onRetry(attempt + 1, delay, axiosError);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await sleep(delay);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// This should never be reached, but TypeScript needs it
|
|
85
|
+
throw lastError;
|
|
86
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authentication store state
|
|
5
|
+
*/
|
|
6
|
+
interface AuthState {
|
|
7
|
+
token: string | null;
|
|
8
|
+
setToken: (token: string) => void;
|
|
9
|
+
clearToken: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Zustand store for managing authentication token
|
|
14
|
+
* Token is stored in memory and used for Authorization header
|
|
15
|
+
*/
|
|
16
|
+
export const useAuthStore = create<AuthState>((set) => ({
|
|
17
|
+
token: null,
|
|
18
|
+
|
|
19
|
+
setToken: (token: string) => {
|
|
20
|
+
set({ token });
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
clearToken: () => {
|
|
24
|
+
set({ token: null });
|
|
25
|
+
},
|
|
26
|
+
}));
|
package/src/api/types.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AxiosRequestConfig } from 'axios';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for API adapter
|
|
5
|
+
*/
|
|
6
|
+
export interface ApiConfig {
|
|
7
|
+
baseURL: string;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
retryAttempts?: number;
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Interface for HTTP client adapter
|
|
15
|
+
* Defines the contract all HTTP adapters must implement
|
|
16
|
+
*/
|
|
17
|
+
export interface IHttpAdapter {
|
|
18
|
+
/**
|
|
19
|
+
* Performs a GET request
|
|
20
|
+
* @template T - Expected response data type
|
|
21
|
+
*/
|
|
22
|
+
get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Performs a POST request
|
|
26
|
+
* @template T - Expected response data type
|
|
27
|
+
*/
|
|
28
|
+
post<T = unknown>(
|
|
29
|
+
url: string,
|
|
30
|
+
data?: unknown,
|
|
31
|
+
config?: AxiosRequestConfig,
|
|
32
|
+
): Promise<T>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Performs a PUT request
|
|
36
|
+
* @template T - Expected response data type
|
|
37
|
+
*/
|
|
38
|
+
put<T = unknown>(
|
|
39
|
+
url: string,
|
|
40
|
+
data?: unknown,
|
|
41
|
+
config?: AxiosRequestConfig,
|
|
42
|
+
): Promise<T>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Performs a DELETE request
|
|
46
|
+
* @template T - Expected response data type
|
|
47
|
+
*/
|
|
48
|
+
delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adds authentication token to all subsequent requests
|
|
52
|
+
*/
|
|
53
|
+
addToken(token: string): void;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Removes authentication token from requests
|
|
57
|
+
*/
|
|
58
|
+
removeToken(): void;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get token from store
|
|
62
|
+
*/
|
|
63
|
+
getToken(): string | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Retry configuration
|
|
68
|
+
*/
|
|
69
|
+
export interface RetryConfig {
|
|
70
|
+
maxAttempts: number;
|
|
71
|
+
baseDelay: number;
|
|
72
|
+
maxDelay: number;
|
|
73
|
+
retryableStatusCodes: number[];
|
|
74
|
+
}
|
package/src/api.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API module entry point.
|
|
3
|
+
* Exports HTTP client, types, errors, and stores.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
ApiError,
|
|
8
|
+
AuthError,
|
|
9
|
+
NetworkError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
ServerError,
|
|
13
|
+
TimeoutError,
|
|
14
|
+
ValidationError,
|
|
15
|
+
} from './api/errors';
|
|
16
|
+
export { AxiosAdapter, apiRequest, useAuthStore } from './api/index';
|
|
17
|
+
export type { ApiConfig, IHttpAdapter } from './api/types';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import styled from '@emotion/native';
|
|
2
|
+
|
|
3
|
+
import { type ButtonProps, ButtonText, DefaultButtonStyle } from './styles';
|
|
4
|
+
|
|
5
|
+
export type { ButtonProps } from './styles';
|
|
6
|
+
|
|
7
|
+
const DefaultButton = (props: ButtonProps) => (
|
|
8
|
+
<DefaultButtonStyle activeOpacity={0.5} {...props} />
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const SquareButton = styled(DefaultButton)(() => ({
|
|
12
|
+
borderRadius: 0,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const OutlineButton = styled(DefaultButton)(() => ({
|
|
16
|
+
borderWidth: 1,
|
|
17
|
+
borderColor: '#333',
|
|
18
|
+
backgroundColor: 'transparent',
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const RoundedButton = styled(DefaultButton)(() => ({
|
|
22
|
+
borderRadius: 8,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const PillButton = styled(DefaultButton)(() => ({
|
|
26
|
+
borderRadius: 999,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const Button = Object.assign(DefaultButton, {
|
|
30
|
+
Square: SquareButton,
|
|
31
|
+
Outline: OutlineButton,
|
|
32
|
+
Rounded: RoundedButton,
|
|
33
|
+
Pill: PillButton,
|
|
34
|
+
Text: ButtonText,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export { Button };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import styled from '@emotion/native';
|
|
2
|
+
import type { DimensionValue, TouchableOpacityProps } from 'react-native';
|
|
3
|
+
import { Text } from '../Text';
|
|
4
|
+
|
|
5
|
+
export type ButtonProps = {
|
|
6
|
+
color?: string;
|
|
7
|
+
paddingVertical?: number;
|
|
8
|
+
paddingHorizontal?: number;
|
|
9
|
+
margin?: DimensionValue;
|
|
10
|
+
} & TouchableOpacityProps;
|
|
11
|
+
|
|
12
|
+
export const DefaultButtonStyle = styled.TouchableOpacity<ButtonProps>(
|
|
13
|
+
({ paddingVertical, paddingHorizontal, color, margin }) => ({
|
|
14
|
+
flexDirection: 'row',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
justifyContent: 'center',
|
|
17
|
+
paddingHorizontal: paddingHorizontal ?? 20,
|
|
18
|
+
paddingVertical: paddingVertical ?? 14,
|
|
19
|
+
gap: 8,
|
|
20
|
+
backgroundColor: color ?? '#333',
|
|
21
|
+
margin,
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const ButtonText = styled(Text)(({ color, size }) => ({
|
|
26
|
+
color: color ?? '#fff',
|
|
27
|
+
fontSize: size ?? 16,
|
|
28
|
+
}));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CardContainer,
|
|
3
|
+
CardContent,
|
|
4
|
+
CardDescription,
|
|
5
|
+
CardImage,
|
|
6
|
+
CardTitle,
|
|
7
|
+
} from './styles';
|
|
8
|
+
import type { CardProps } from './types';
|
|
9
|
+
|
|
10
|
+
export type { CardProps } from './types';
|
|
11
|
+
|
|
12
|
+
const CardRoot = (props: CardProps) => (
|
|
13
|
+
<CardContainer activeOpacity={0.7} {...props} />
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const Card = Object.assign(CardRoot, {
|
|
17
|
+
Image: CardImage,
|
|
18
|
+
Content: CardContent,
|
|
19
|
+
Title: CardTitle,
|
|
20
|
+
Description: CardDescription,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export { Card };
|