@cranberry-money/shared-services 1.0.0
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/README.md +288 -0
- package/dist/adapters/MobileApiClient.d.ts +68 -0
- package/dist/adapters/MobileApiClient.d.ts.map +1 -0
- package/dist/adapters/MobileApiClient.js +240 -0
- package/dist/adapters/MobileTokenStorage.d.ts +43 -0
- package/dist/adapters/MobileTokenStorage.d.ts.map +1 -0
- package/dist/adapters/MobileTokenStorage.js +128 -0
- package/dist/adapters/WebApiClient.d.ts +28 -0
- package/dist/adapters/WebApiClient.d.ts.map +1 -0
- package/dist/adapters/WebApiClient.js +119 -0
- package/dist/adapters/WebTokenStorage.d.ts +38 -0
- package/dist/adapters/WebTokenStorage.d.ts.map +1 -0
- package/dist/adapters/WebTokenStorage.js +86 -0
- package/dist/auth/AuthManager.d.ts +81 -0
- package/dist/auth/AuthManager.d.ts.map +1 -0
- package/dist/auth/AuthManager.js +223 -0
- package/dist/auth/createAuthManager.d.ts +63 -0
- package/dist/auth/createAuthManager.d.ts.map +1 -0
- package/dist/auth/createAuthManager.js +103 -0
- package/dist/auth/useAuthManager.d.ts +66 -0
- package/dist/auth/useAuthManager.d.ts.map +1 -0
- package/dist/auth/useAuthManager.js +133 -0
- package/dist/core/BaseApiClient.d.ts +82 -0
- package/dist/core/BaseApiClient.d.ts.map +1 -0
- package/dist/core/BaseApiClient.js +89 -0
- package/dist/core/TokenStorage.d.ts +45 -0
- package/dist/core/TokenStorage.d.ts.map +1 -0
- package/dist/core/TokenStorage.js +23 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/query/QueryClient.d.ts +82 -0
- package/dist/query/QueryClient.d.ts.map +1 -0
- package/dist/query/QueryClient.js +136 -0
- package/dist/query/useAuth.d.ts +64 -0
- package/dist/query/useAuth.d.ts.map +1 -0
- package/dist/query/useAuth.js +144 -0
- package/dist/query/usePortfolios.d.ts +79 -0
- package/dist/query/usePortfolios.d.ts.map +1 -0
- package/dist/query/usePortfolios.js +172 -0
- package/dist/services/AuthService.d.ts +75 -0
- package/dist/services/AuthService.d.ts.map +1 -0
- package/dist/services/AuthService.js +83 -0
- package/dist/services/BaseService.d.ts +48 -0
- package/dist/services/BaseService.d.ts.map +1 -0
- package/dist/services/BaseService.js +51 -0
- package/dist/services/PortfolioService.d.ts +100 -0
- package/dist/services/PortfolioService.d.ts.map +1 -0
- package/dist/services/PortfolioService.js +68 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# @myportfolio/shared-services
|
|
2
|
+
|
|
3
|
+
Shared API services and client abstractions for the MyPortfolio platform, supporting both web (Blueberry) and mobile (Blackberry) applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @myportfolio/shared-services
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Core Architecture
|
|
12
|
+
|
|
13
|
+
This package provides a platform-agnostic API client architecture with:
|
|
14
|
+
|
|
15
|
+
- **Token Storage Abstraction**: Secure token management across platforms
|
|
16
|
+
- **Base API Client**: Common HTTP request interface
|
|
17
|
+
- **Platform Adapters**: Web (cookies) and Mobile (secure storage) implementations
|
|
18
|
+
- **Offline Support**: Request queuing and retry logic for mobile
|
|
19
|
+
- **Authentication Handling**: Automatic token refresh and error handling
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Web Implementation
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { WebApiClient, WebTokenStorage } from '@myportfolio/shared-services';
|
|
27
|
+
|
|
28
|
+
// Create web API client with cookie-based auth
|
|
29
|
+
const apiClient = new WebApiClient({
|
|
30
|
+
baseURL: 'https://api.myportfolio.com',
|
|
31
|
+
withCredentials: true, // Enable cookies
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Set up token storage (optional for cookie-based auth)
|
|
35
|
+
const tokenStorage = new WebTokenStorage();
|
|
36
|
+
apiClient.setTokenStorage(tokenStorage);
|
|
37
|
+
|
|
38
|
+
// Make requests
|
|
39
|
+
const response = await apiClient.get('/auth/profile');
|
|
40
|
+
const data = await apiClient.post('/portfolios', { name: 'My Portfolio' });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Mobile Implementation
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import {
|
|
47
|
+
MobileApiClient,
|
|
48
|
+
MobileTokenStorage,
|
|
49
|
+
createExpoSecureStoreAdapter
|
|
50
|
+
} from '@myportfolio/shared-services';
|
|
51
|
+
|
|
52
|
+
// Create mobile API client with secure token storage
|
|
53
|
+
const apiClient = new MobileApiClient({
|
|
54
|
+
baseURL: 'https://api.myportfolio.com',
|
|
55
|
+
retryAttempts: 3,
|
|
56
|
+
offlineQueueEnabled: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Set up secure token storage
|
|
60
|
+
const secureStore = createExpoSecureStoreAdapter();
|
|
61
|
+
const tokenStorage = new MobileTokenStorage(secureStore);
|
|
62
|
+
apiClient.setTokenStorage(tokenStorage);
|
|
63
|
+
|
|
64
|
+
// Handle network state
|
|
65
|
+
apiClient.setNetworkState(isOnline);
|
|
66
|
+
|
|
67
|
+
// Make requests with automatic retry and offline queueing
|
|
68
|
+
const response = await apiClient.get('/auth/profile');
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Core Components
|
|
72
|
+
|
|
73
|
+
### TokenStorage Interface
|
|
74
|
+
|
|
75
|
+
Provides secure, cross-platform token management:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
interface TokenStorage {
|
|
79
|
+
storeTokens(tokens: { access: string; refresh: string }): Promise<void>;
|
|
80
|
+
retrieveTokens(): Promise<{ access: string; refresh: string } | null>;
|
|
81
|
+
clearTokens(): Promise<void>;
|
|
82
|
+
hasTokens(): Promise<boolean>;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Implementations:**
|
|
87
|
+
- `WebTokenStorage` - HTTP-only cookies (web)
|
|
88
|
+
- `MobileTokenStorage` - Secure device storage (mobile)
|
|
89
|
+
|
|
90
|
+
### BaseApiClient
|
|
91
|
+
|
|
92
|
+
Abstract base class providing common API functionality:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
abstract class BaseApiClient {
|
|
96
|
+
// HTTP methods
|
|
97
|
+
get<T>(url: string, params?: Record<string, unknown>): Promise<ApiResponse<T>>;
|
|
98
|
+
post<T>(url: string, data?: unknown): Promise<ApiResponse<T>>;
|
|
99
|
+
put<T>(url: string, data?: unknown): Promise<ApiResponse<T>>;
|
|
100
|
+
patch<T>(url: string, data?: unknown): Promise<ApiResponse<T>>;
|
|
101
|
+
delete<T>(url: string): Promise<ApiResponse<T>>;
|
|
102
|
+
|
|
103
|
+
// Configuration
|
|
104
|
+
setTokenStorage(storage: TokenStorage): void;
|
|
105
|
+
|
|
106
|
+
// Abstract methods (implemented by platform adapters)
|
|
107
|
+
abstract request<T>(config: RequestConfig): Promise<ApiResponse<T>>;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Platform Adapters
|
|
112
|
+
|
|
113
|
+
### Web Adapter (`WebApiClient`)
|
|
114
|
+
|
|
115
|
+
Optimized for browser environments:
|
|
116
|
+
|
|
117
|
+
- **Cookie-based authentication** with `withCredentials` support
|
|
118
|
+
- **CORS handling** for cross-origin requests
|
|
119
|
+
- **Fetch API** based implementation
|
|
120
|
+
- **Content-type detection** for response parsing
|
|
121
|
+
- **Token refresh** via `/auth/refresh` endpoint
|
|
122
|
+
|
|
123
|
+
### Mobile Adapter (`MobileApiClient`)
|
|
124
|
+
|
|
125
|
+
Enhanced for React Native with mobile-specific features:
|
|
126
|
+
|
|
127
|
+
- **Retry logic** with exponential backoff
|
|
128
|
+
- **Offline queue** for when network is unavailable
|
|
129
|
+
- **Network state awareness** with automatic queue processing
|
|
130
|
+
- **Timeout handling** with AbortSignal
|
|
131
|
+
- **Secure token storage** integration
|
|
132
|
+
- **Battery-conscious** request batching
|
|
133
|
+
|
|
134
|
+
## Authentication Flow
|
|
135
|
+
|
|
136
|
+
### Web (Cookie-based)
|
|
137
|
+
1. User signs in → Server sets HTTP-only cookies
|
|
138
|
+
2. API requests automatically include cookies
|
|
139
|
+
3. Token refresh handled by server cookie renewal
|
|
140
|
+
4. Sign out clears cookies on server
|
|
141
|
+
|
|
142
|
+
### Mobile (Token-based)
|
|
143
|
+
1. User signs in → Tokens stored in secure storage
|
|
144
|
+
2. API requests include `Authorization: Bearer <token>` header
|
|
145
|
+
3. Automatic token refresh with secure storage update
|
|
146
|
+
4. Sign out clears tokens from secure storage
|
|
147
|
+
|
|
148
|
+
## Error Handling
|
|
149
|
+
|
|
150
|
+
Consistent error handling across platforms:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
interface ApiError {
|
|
154
|
+
message: string;
|
|
155
|
+
status?: number;
|
|
156
|
+
code?: string;
|
|
157
|
+
data?: unknown;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Error codes
|
|
161
|
+
'NETWORK_ERROR' // Network connectivity issues
|
|
162
|
+
'TIMEOUT_ERROR' // Request timeout
|
|
163
|
+
'HTTP_ERROR' // HTTP status errors (4xx, 5xx)
|
|
164
|
+
'UNAUTHORIZED' // Authentication failure
|
|
165
|
+
'UNEXPECTED_ERROR' // Unknown errors
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Offline Support (Mobile)
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// Enable offline queue
|
|
172
|
+
const client = new MobileApiClient({
|
|
173
|
+
offlineQueueEnabled: true
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Set network state
|
|
177
|
+
client.setNetworkState(false); // Requests queued
|
|
178
|
+
client.setNetworkState(true); // Queue processed
|
|
179
|
+
|
|
180
|
+
// Manual sync
|
|
181
|
+
await client.syncWhenOnline();
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Token Storage Security
|
|
185
|
+
|
|
186
|
+
### Web Security
|
|
187
|
+
- HTTP-only cookies prevent XSS attacks
|
|
188
|
+
- SameSite cookie attributes prevent CSRF
|
|
189
|
+
- Secure flag for HTTPS-only transmission
|
|
190
|
+
- Server-side session management
|
|
191
|
+
|
|
192
|
+
### Mobile Security
|
|
193
|
+
- iOS Keychain / Android Keystore integration
|
|
194
|
+
- Hardware-backed encryption when available
|
|
195
|
+
- Automatic data protection classes
|
|
196
|
+
- Secure deletion on app uninstall
|
|
197
|
+
|
|
198
|
+
## React Query Integration
|
|
199
|
+
|
|
200
|
+
The package is designed to work seamlessly with React Query:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// Custom hook example
|
|
204
|
+
function useProfile() {
|
|
205
|
+
return useQuery({
|
|
206
|
+
queryKey: ['profile'],
|
|
207
|
+
queryFn: () => apiClient.get('/auth/profile'),
|
|
208
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Mutation example
|
|
213
|
+
function useSignIn() {
|
|
214
|
+
return useMutation({
|
|
215
|
+
mutationFn: (credentials) => apiClient.post('/auth/signin', credentials),
|
|
216
|
+
onSuccess: (response) => {
|
|
217
|
+
// Handle successful authentication
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Configuration
|
|
224
|
+
|
|
225
|
+
### Web Configuration
|
|
226
|
+
```typescript
|
|
227
|
+
const client = new WebApiClient({
|
|
228
|
+
baseURL: 'https://api.myportfolio.com',
|
|
229
|
+
timeout: 30000,
|
|
230
|
+
withCredentials: true,
|
|
231
|
+
defaultHeaders: {
|
|
232
|
+
'X-Client-Version': '1.0.0',
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Mobile Configuration
|
|
238
|
+
```typescript
|
|
239
|
+
const client = new MobileApiClient({
|
|
240
|
+
baseURL: 'https://api.myportfolio.com',
|
|
241
|
+
timeout: 30000,
|
|
242
|
+
retryAttempts: 3,
|
|
243
|
+
retryDelay: 1000,
|
|
244
|
+
offlineQueueEnabled: true,
|
|
245
|
+
defaultHeaders: {
|
|
246
|
+
'X-Client-Version': '1.0.0',
|
|
247
|
+
'X-Platform': 'mobile',
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Development
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# Build the package
|
|
256
|
+
npm run build
|
|
257
|
+
|
|
258
|
+
# Watch for changes
|
|
259
|
+
npm run dev
|
|
260
|
+
|
|
261
|
+
# Type check
|
|
262
|
+
npm run typecheck
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Dependencies
|
|
266
|
+
|
|
267
|
+
- `@myportfolio/shared-types` - Shared type definitions
|
|
268
|
+
- `@myportfolio/shared-constants` - HTTP constants and headers
|
|
269
|
+
|
|
270
|
+
## Peer Dependencies
|
|
271
|
+
|
|
272
|
+
- `@tanstack/react-query` - For React Query integration (optional)
|
|
273
|
+
- `typescript` - For TypeScript support
|
|
274
|
+
|
|
275
|
+
## Platform Requirements
|
|
276
|
+
|
|
277
|
+
### Web
|
|
278
|
+
- Modern browsers with Fetch API support
|
|
279
|
+
- Cookie support for authentication
|
|
280
|
+
|
|
281
|
+
### Mobile
|
|
282
|
+
- React Native 0.60+
|
|
283
|
+
- Expo SDK 47+ (for Expo SecureStore)
|
|
284
|
+
- iOS 10+ / Android API 21+
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
MIT
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile-specific API client implementation
|
|
3
|
+
*
|
|
4
|
+
* This implementation is designed for React Native environments
|
|
5
|
+
* and includes mobile-specific features like network state awareness,
|
|
6
|
+
* offline capability, and retry logic.
|
|
7
|
+
*/
|
|
8
|
+
import { BaseApiClient, type RequestConfig, type ApiResponse, type ApiClientConfig } from '../core/BaseApiClient';
|
|
9
|
+
export interface MobileApiClientConfig extends ApiClientConfig {
|
|
10
|
+
retryAttempts?: number;
|
|
11
|
+
retryDelay?: number;
|
|
12
|
+
offlineQueueEnabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare class MobileApiClient extends BaseApiClient {
|
|
15
|
+
private retryAttempts;
|
|
16
|
+
private retryDelay;
|
|
17
|
+
private offlineQueueEnabled;
|
|
18
|
+
private offlineQueue;
|
|
19
|
+
private isOnline;
|
|
20
|
+
constructor(config: MobileApiClientConfig);
|
|
21
|
+
request<T = unknown>(config: RequestConfig): Promise<ApiResponse<T>>;
|
|
22
|
+
private executeRequest;
|
|
23
|
+
/**
|
|
24
|
+
* Queue request for when back online
|
|
25
|
+
*/
|
|
26
|
+
private queueRequest;
|
|
27
|
+
/**
|
|
28
|
+
* Process offline queue when back online
|
|
29
|
+
*/
|
|
30
|
+
syncWhenOnline(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Enable offline mode
|
|
33
|
+
*/
|
|
34
|
+
enableOfflineMode(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Enable online mode and sync queue
|
|
37
|
+
*/
|
|
38
|
+
enableOnlineMode(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Set network state
|
|
41
|
+
*/
|
|
42
|
+
setNetworkState(isOnline: boolean): void;
|
|
43
|
+
/**
|
|
44
|
+
* Parse response data based on content type
|
|
45
|
+
*/
|
|
46
|
+
private parseResponseData;
|
|
47
|
+
/**
|
|
48
|
+
* Convert Headers to plain object
|
|
49
|
+
*/
|
|
50
|
+
private parseResponseHeaders;
|
|
51
|
+
/**
|
|
52
|
+
* Check if error is an API error
|
|
53
|
+
*/
|
|
54
|
+
private isApiError;
|
|
55
|
+
/**
|
|
56
|
+
* Determine if request should be retried
|
|
57
|
+
*/
|
|
58
|
+
private shouldRetry;
|
|
59
|
+
/**
|
|
60
|
+
* Handle token refresh for mobile (secure storage based)
|
|
61
|
+
*/
|
|
62
|
+
protected handleTokenRefresh(): Promise<boolean>;
|
|
63
|
+
/**
|
|
64
|
+
* Utility delay function
|
|
65
|
+
*/
|
|
66
|
+
private delay;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=MobileApiClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MobileApiClient.d.ts","sourceRoot":"","sources":["../../src/adapters/MobileApiClient.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAElH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,qBAAa,eAAgB,SAAQ,aAAa;IAChD,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,mBAAmB,CAAU;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,QAAQ,CAAiB;gBAErB,MAAM,EAAE,qBAAqB;IAcnC,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;YAS5D,cAAc;IAwF5B;;OAEG;YACW,YAAY;IAU1B;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBrC;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAIzB;;OAEG;IACG,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAOvC;;OAEG;IACH,eAAe,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;IAUxC;;OAEG;YACW,iBAAiB;IAe/B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAQ5B;;OAEG;IACH,OAAO,CAAC,UAAU;IAOlB;;OAEG;IACH,OAAO,CAAC,WAAW;IAsBnB;;OAEG;cACa,kBAAkB,IAAI,OAAO,CAAC,OAAO,CAAC;IAyBtD;;OAEG;IACH,OAAO,CAAC,KAAK;CAGd"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile-specific API client implementation
|
|
3
|
+
*
|
|
4
|
+
* This implementation is designed for React Native environments
|
|
5
|
+
* and includes mobile-specific features like network state awareness,
|
|
6
|
+
* offline capability, and retry logic.
|
|
7
|
+
*/
|
|
8
|
+
import { HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON } from '@myportfolio/shared-constants';
|
|
9
|
+
import { BaseApiClient } from '../core/BaseApiClient';
|
|
10
|
+
export class MobileApiClient extends BaseApiClient {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
super({
|
|
13
|
+
...config,
|
|
14
|
+
defaultHeaders: {
|
|
15
|
+
[HTTP_HEADER_CONTENT_TYPE]: CONTENT_TYPE_APPLICATION_JSON,
|
|
16
|
+
...config.defaultHeaders,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
this.offlineQueue = [];
|
|
20
|
+
this.isOnline = true;
|
|
21
|
+
this.retryAttempts = config.retryAttempts || 3;
|
|
22
|
+
this.retryDelay = config.retryDelay || 1000;
|
|
23
|
+
this.offlineQueueEnabled = config.offlineQueueEnabled || false;
|
|
24
|
+
}
|
|
25
|
+
async request(config) {
|
|
26
|
+
// Check if we're offline and should queue the request
|
|
27
|
+
if (!this.isOnline && this.offlineQueueEnabled) {
|
|
28
|
+
return this.queueRequest(config);
|
|
29
|
+
}
|
|
30
|
+
return this.executeRequest(config);
|
|
31
|
+
}
|
|
32
|
+
async executeRequest(config, attempt = 1) {
|
|
33
|
+
const url = this.buildUrl(config.url);
|
|
34
|
+
const headers = await this.buildHeaders(config.headers);
|
|
35
|
+
// Build fetch options
|
|
36
|
+
const fetchOptions = {
|
|
37
|
+
method: config.method || 'GET',
|
|
38
|
+
headers,
|
|
39
|
+
signal: config.timeout ? AbortSignal.timeout(config.timeout) : undefined,
|
|
40
|
+
};
|
|
41
|
+
// Add body for non-GET requests
|
|
42
|
+
if (config.data && config.method !== 'GET') {
|
|
43
|
+
if (headers[HTTP_HEADER_CONTENT_TYPE] === CONTENT_TYPE_APPLICATION_JSON) {
|
|
44
|
+
fetchOptions.body = JSON.stringify(config.data);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
fetchOptions.body = config.data;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Add query parameters for GET requests
|
|
51
|
+
const requestUrl = config.params && config.method === 'GET'
|
|
52
|
+
? `${url}?${new URLSearchParams(config.params).toString()}`
|
|
53
|
+
: url;
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(requestUrl, fetchOptions);
|
|
56
|
+
// Handle non-2xx responses
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const errorData = await this.parseResponseData(response);
|
|
59
|
+
// Handle authentication errors
|
|
60
|
+
if (response.status === 401) {
|
|
61
|
+
const refreshed = await this.handleTokenRefresh();
|
|
62
|
+
if (refreshed && attempt === 1) {
|
|
63
|
+
// Retry once with new token
|
|
64
|
+
return this.executeRequest(config, attempt + 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw this.createError(response.statusText || 'Request failed', response.status, 'HTTP_ERROR', errorData);
|
|
68
|
+
}
|
|
69
|
+
const data = await this.parseResponseData(response);
|
|
70
|
+
return {
|
|
71
|
+
data,
|
|
72
|
+
status: response.status,
|
|
73
|
+
statusText: response.statusText,
|
|
74
|
+
headers: this.parseResponseHeaders(response.headers),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Handle network errors with retry logic
|
|
79
|
+
if (this.shouldRetry(error, attempt)) {
|
|
80
|
+
await this.delay(this.retryDelay * attempt); // Exponential backoff
|
|
81
|
+
return this.executeRequest(config, attempt + 1);
|
|
82
|
+
}
|
|
83
|
+
// Handle different error types
|
|
84
|
+
if (error instanceof TypeError && error.message.includes('fetch')) {
|
|
85
|
+
throw this.createError('Network error', 0, 'NETWORK_ERROR');
|
|
86
|
+
}
|
|
87
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
88
|
+
throw this.createError('Request timeout', 0, 'TIMEOUT_ERROR');
|
|
89
|
+
}
|
|
90
|
+
// Re-throw API errors
|
|
91
|
+
if (this.isApiError(error)) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
// Handle unexpected errors
|
|
95
|
+
throw this.createError(error instanceof Error ? error.message : 'Unknown error', 0, 'UNEXPECTED_ERROR');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Queue request for when back online
|
|
100
|
+
*/
|
|
101
|
+
async queueRequest(config) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
this.offlineQueue.push({
|
|
104
|
+
...config,
|
|
105
|
+
resolve: resolve,
|
|
106
|
+
reject,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Process offline queue when back online
|
|
112
|
+
*/
|
|
113
|
+
async syncWhenOnline() {
|
|
114
|
+
if (!this.isOnline || this.offlineQueue.length === 0)
|
|
115
|
+
return;
|
|
116
|
+
const queue = [...this.offlineQueue];
|
|
117
|
+
this.offlineQueue = [];
|
|
118
|
+
for (const request of queue) {
|
|
119
|
+
try {
|
|
120
|
+
const response = await this.executeRequest(request);
|
|
121
|
+
request.resolve(response);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
request.reject(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Enable offline mode
|
|
130
|
+
*/
|
|
131
|
+
enableOfflineMode() {
|
|
132
|
+
this.isOnline = false;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Enable online mode and sync queue
|
|
136
|
+
*/
|
|
137
|
+
async enableOnlineMode() {
|
|
138
|
+
this.isOnline = true;
|
|
139
|
+
if (this.offlineQueueEnabled) {
|
|
140
|
+
await this.syncWhenOnline();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Set network state
|
|
145
|
+
*/
|
|
146
|
+
setNetworkState(isOnline) {
|
|
147
|
+
const wasOffline = !this.isOnline;
|
|
148
|
+
this.isOnline = isOnline;
|
|
149
|
+
if (wasOffline && isOnline && this.offlineQueueEnabled) {
|
|
150
|
+
// Process queue when coming back online
|
|
151
|
+
this.syncWhenOnline().catch(console.error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Parse response data based on content type
|
|
156
|
+
*/
|
|
157
|
+
async parseResponseData(response) {
|
|
158
|
+
const contentType = response.headers.get('content-type') || '';
|
|
159
|
+
if (contentType.includes('application/json')) {
|
|
160
|
+
return response.json();
|
|
161
|
+
}
|
|
162
|
+
if (contentType.includes('text/')) {
|
|
163
|
+
return response.text();
|
|
164
|
+
}
|
|
165
|
+
// For other content types, return as blob
|
|
166
|
+
return response.blob();
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Convert Headers to plain object
|
|
170
|
+
*/
|
|
171
|
+
parseResponseHeaders(headers) {
|
|
172
|
+
const headerObj = {};
|
|
173
|
+
headers.forEach((value, key) => {
|
|
174
|
+
headerObj[key] = value;
|
|
175
|
+
});
|
|
176
|
+
return headerObj;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Check if error is an API error
|
|
180
|
+
*/
|
|
181
|
+
isApiError(error) {
|
|
182
|
+
return typeof error === 'object' &&
|
|
183
|
+
error !== null &&
|
|
184
|
+
'message' in error &&
|
|
185
|
+
'status' in error;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Determine if request should be retried
|
|
189
|
+
*/
|
|
190
|
+
shouldRetry(error, attempt) {
|
|
191
|
+
if (attempt >= this.retryAttempts)
|
|
192
|
+
return false;
|
|
193
|
+
// Retry on network errors
|
|
194
|
+
if (error instanceof TypeError && error.message.includes('fetch')) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
// Retry on timeout errors
|
|
198
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
// Retry on certain HTTP status codes
|
|
202
|
+
if (this.isApiError(error)) {
|
|
203
|
+
const status = error.status;
|
|
204
|
+
return status === 429 || (status !== undefined && status >= 500); // Rate limit or server errors
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Handle token refresh for mobile (secure storage based)
|
|
210
|
+
*/
|
|
211
|
+
async handleTokenRefresh() {
|
|
212
|
+
if (!this.tokenStorage)
|
|
213
|
+
return false;
|
|
214
|
+
try {
|
|
215
|
+
const tokens = await this.tokenStorage.retrieveTokens();
|
|
216
|
+
if (!tokens?.refresh)
|
|
217
|
+
return false;
|
|
218
|
+
const response = await this.request({
|
|
219
|
+
url: '/auth/refresh',
|
|
220
|
+
method: 'POST',
|
|
221
|
+
data: { refresh: tokens.refresh },
|
|
222
|
+
});
|
|
223
|
+
if (response.status === 200 && response.data) {
|
|
224
|
+
const newTokens = response.data;
|
|
225
|
+
await this.tokenStorage.storeTokens(newTokens);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Utility delay function
|
|
236
|
+
*/
|
|
237
|
+
delay(ms) {
|
|
238
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile-specific token storage using secure storage
|
|
3
|
+
*
|
|
4
|
+
* This implementation uses platform-secure storage mechanisms
|
|
5
|
+
* like iOS Keychain and Android Keystore via React Native libraries.
|
|
6
|
+
*
|
|
7
|
+
* For Expo: Uses Expo SecureStore
|
|
8
|
+
* For bare React Native: Would use @react-native-async-storage/async-storage
|
|
9
|
+
* with encryption or react-native-keychain
|
|
10
|
+
*/
|
|
11
|
+
import { BaseTokenStorage, type TokenPair } from '../core/TokenStorage';
|
|
12
|
+
export declare class MobileTokenStorage extends BaseTokenStorage {
|
|
13
|
+
private secureStore;
|
|
14
|
+
constructor(secureStore: SecureStoreInterface);
|
|
15
|
+
storeTokens(tokens: TokenPair): Promise<void>;
|
|
16
|
+
retrieveTokens(): Promise<TokenPair | null>;
|
|
17
|
+
clearTokens(): Promise<void>;
|
|
18
|
+
hasTokens(): Promise<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Check if secure storage is available
|
|
21
|
+
*/
|
|
22
|
+
isAvailable(): Promise<boolean>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Interface for secure storage implementations
|
|
26
|
+
* This abstracts the specific secure storage library being used
|
|
27
|
+
*/
|
|
28
|
+
export interface SecureStoreInterface {
|
|
29
|
+
setItemAsync(key: string, value: string): Promise<void>;
|
|
30
|
+
getItemAsync(key: string): Promise<string | null>;
|
|
31
|
+
deleteItemAsync(key: string): Promise<void>;
|
|
32
|
+
isAvailableAsync?(): Promise<boolean>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Factory function to create MobileTokenStorage with different backends
|
|
36
|
+
*/
|
|
37
|
+
export declare const createMobileTokenStorage: (secureStore: SecureStoreInterface) => MobileTokenStorage;
|
|
38
|
+
/**
|
|
39
|
+
* Expo SecureStore adapter
|
|
40
|
+
* Usage: createMobileTokenStorage(createExpoSecureStoreAdapter())
|
|
41
|
+
*/
|
|
42
|
+
export declare const createExpoSecureStoreAdapter: () => SecureStoreInterface;
|
|
43
|
+
//# sourceMappingURL=MobileTokenStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MobileTokenStorage.d.ts","sourceRoot":"","sources":["../../src/adapters/MobileTokenStorage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAQxE,qBAAa,kBAAmB,SAAQ,gBAAgB;IACtD,OAAO,CAAC,WAAW,CAAuB;gBAE9B,WAAW,EAAE,oBAAoB;IAKvC,WAAW,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAe7C,cAAc,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA0B3C,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAanC;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;CAStC;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAClD,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,gBAAgB,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACvC;AAED;;GAEG;AACH,eAAO,MAAM,wBAAwB,GAAI,aAAa,oBAAoB,KAAG,kBAE5E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,QAAO,oBAsB/C,CAAC"}
|