more package declarations
This commit is contained in:
249
packages/mobile-api-client/src/api/api-client.ts
Normal file
249
packages/mobile-api-client/src/api/api-client.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* API Client for ShieldAI services
|
||||
* Handles authentication, request/response interception, and error handling
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
|
||||
import { tokenStorage } from '../storage/token-storage';
|
||||
import { requestQueue } from '../utils/request-queue';
|
||||
import type { AuthTokens, AuthResponse, RefreshTokenRequest } from '../types';
|
||||
|
||||
export interface ApiClientConfig {
|
||||
baseURL: string;
|
||||
timeout?: number;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
private client: AxiosInstance;
|
||||
private config: ApiClientConfig;
|
||||
private isRefreshing = false;
|
||||
private refreshSubscribers: Set<(token: string) => void> = new Set();
|
||||
|
||||
constructor(config: ApiClientConfig) {
|
||||
this.config = {
|
||||
baseURL: config.baseURL,
|
||||
timeout: config.timeout ?? 30000,
|
||||
debug: config.debug ?? false,
|
||||
};
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: this.config.baseURL,
|
||||
timeout: this.config.timeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
private setupInterceptors(): void {
|
||||
// Request interceptor - add auth token
|
||||
this.client.interceptors.request.use(
|
||||
async (config) => {
|
||||
if (this.config.debug) {
|
||||
console.log('[API] Request:', config.method?.toUpperCase(), config.url);
|
||||
}
|
||||
|
||||
// Add auth token if available
|
||||
const token = await tokenStorage.getAccessToken();
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// Queue request if offline
|
||||
if (!requestQueue.isOnline() && this.requiresNetwork(config)) {
|
||||
await requestQueue.enqueue(config);
|
||||
throw new Error('OFFLINE');
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
if (error.message === 'OFFLINE') {
|
||||
return Promise.reject({ offline: true, config: error.config });
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor - handle errors and token refresh
|
||||
this.client.interceptors.response.use(
|
||||
(response) => {
|
||||
if (this.config.debug) {
|
||||
console.log('[API] Response:', response.status, response.config.url);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
async (error: AxiosError) => {
|
||||
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
|
||||
|
||||
// Handle 401 - unauthorized
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
if (this.isRefreshing) {
|
||||
// Wait for refresh to complete
|
||||
return new Promise((resolve) => {
|
||||
this.refreshSubscribers.add((token) => {
|
||||
originalRequest.headers = originalRequest.headers || {};
|
||||
originalRequest.headers.Authorization = `Bearer ${token}`;
|
||||
resolve(this.client(originalRequest));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
originalRequest._retry = true;
|
||||
this.isRefreshing = true;
|
||||
|
||||
try {
|
||||
const refreshToken = await tokenStorage.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token');
|
||||
}
|
||||
|
||||
const newTokens = await this.refreshAccessToken(refreshToken);
|
||||
await tokenStorage.saveTokens(newTokens.accessToken, newTokens.refreshToken);
|
||||
|
||||
// Retry failed requests
|
||||
this.refreshSubscribers.forEach((callback) => callback(newTokens.accessToken));
|
||||
this.refreshSubscribers.clear();
|
||||
|
||||
originalRequest.headers = originalRequest.headers || {};
|
||||
originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`;
|
||||
|
||||
return this.client(originalRequest);
|
||||
} catch (refreshError) {
|
||||
// Refresh failed - clear tokens and redirect to login
|
||||
await tokenStorage.clearTokens();
|
||||
this.refreshSubscribers.clear();
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private requiresNetwork(config: AxiosRequestConfig): boolean {
|
||||
// Don't queue GET requests that can be cached
|
||||
const method = (config.method || 'get').toLowerCase();
|
||||
return method !== 'get';
|
||||
}
|
||||
|
||||
private async refreshAccessToken(refreshToken: string): Promise<AuthTokens> {
|
||||
const response = await this.client.post<AuthResponse>('/auth/refresh', {
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: response.data.tokens.accessToken,
|
||||
refreshToken: response.data.tokens.refreshToken,
|
||||
expiresIn: response.data.tokens.expiresIn,
|
||||
tokenType: response.data.tokens.tokenType,
|
||||
};
|
||||
}
|
||||
|
||||
// Subscribe to token refresh
|
||||
onTokenRefresh(callback: (token: string) => void): () => void {
|
||||
this.refreshSubscribers.add(callback);
|
||||
return () => this.refreshSubscribers.delete(callback);
|
||||
}
|
||||
|
||||
// Public API methods
|
||||
async get<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.get<T>(url, config);
|
||||
}
|
||||
|
||||
async post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.post<T>(url, data, config);
|
||||
}
|
||||
|
||||
async put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.put<T>(url, data, config);
|
||||
}
|
||||
|
||||
async patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.patch<T>(url, data, config);
|
||||
}
|
||||
|
||||
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
return this.client.delete<T>(url, config);
|
||||
}
|
||||
|
||||
// Auth methods
|
||||
async login(email: string, password: string): Promise<AuthResponse> {
|
||||
const response = await this.post<AuthResponse>('/auth/login', {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (response.data.tokens) {
|
||||
await tokenStorage.saveTokens(
|
||||
response.data.tokens.accessToken,
|
||||
response.data.tokens.refreshToken
|
||||
);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async register(data: {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}): Promise<AuthResponse> {
|
||||
const response = await this.post<AuthResponse>('/auth/register', data);
|
||||
|
||||
if (response.data.tokens) {
|
||||
await tokenStorage.saveTokens(
|
||||
response.data.tokens.accessToken,
|
||||
response.data.tokens.refreshToken
|
||||
);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
await this.post('/auth/logout');
|
||||
} finally {
|
||||
await tokenStorage.clearTokens();
|
||||
}
|
||||
}
|
||||
|
||||
async isAuthenticated(): Promise<boolean> {
|
||||
const token = await tokenStorage.getAccessToken();
|
||||
return !!token;
|
||||
}
|
||||
|
||||
// Health check
|
||||
async healthCheck(): Promise<{ status: string; version: string }> {
|
||||
const response = await this.get<{ status: string; version: string }>('/health');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Get the underlying axios instance for advanced usage
|
||||
getClient(): AxiosInstance {
|
||||
return this.client;
|
||||
}
|
||||
}
|
||||
|
||||
// Default client instance
|
||||
let defaultClient: ApiClient | null = null;
|
||||
|
||||
export const createApiClient = (config: ApiClientConfig): ApiClient => {
|
||||
defaultClient = new ApiClient(config);
|
||||
return defaultClient;
|
||||
};
|
||||
|
||||
export const getApiClient = (): ApiClient => {
|
||||
if (!defaultClient) {
|
||||
throw new Error('API Client not initialized. Call createApiClient first.');
|
||||
}
|
||||
return defaultClient;
|
||||
};
|
||||
46
packages/mobile-api-client/src/api/auth.service.ts
Normal file
46
packages/mobile-api-client/src/api/auth.service.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Authentication API service
|
||||
*/
|
||||
|
||||
import { getApiClient } from './api-client';
|
||||
import type {
|
||||
User,
|
||||
AuthResponse,
|
||||
LoginCredentials,
|
||||
RegisterData,
|
||||
AuthTokens
|
||||
} from '../types';
|
||||
|
||||
export class AuthService {
|
||||
private api = getApiClient();
|
||||
|
||||
async login(credentials: LoginCredentials): Promise<AuthResponse> {
|
||||
const response = await this.api.login(credentials.email, credentials.password);
|
||||
return response;
|
||||
}
|
||||
|
||||
async register(data: RegisterData): Promise<AuthResponse> {
|
||||
const response = await this.api.register(data);
|
||||
return response;
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.api.logout();
|
||||
}
|
||||
|
||||
async getCurrentUser(): Promise<User> {
|
||||
const response = await this.api.get<{ user: User; authType: string }>('/auth/user/me');
|
||||
return response.data.user;
|
||||
}
|
||||
|
||||
async refreshToken(): Promise<AuthTokens> {
|
||||
const response = await this.api.get<AuthTokens>('/auth/refresh-token');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async isAuthenticated(): Promise<boolean> {
|
||||
return await this.api.isAuthenticated();
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = new AuthService();
|
||||
47
packages/mobile-api-client/src/api/device.service.ts
Normal file
47
packages/mobile-api-client/src/api/device.service.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Device API service
|
||||
*/
|
||||
|
||||
import { getApiClient } from './api-client';
|
||||
import type { Device, DeviceRegistration, DeviceListResponse } from '../types';
|
||||
|
||||
export class DeviceService {
|
||||
private api = getApiClient();
|
||||
|
||||
async registerDevice(data: DeviceRegistration): Promise<Device> {
|
||||
const response = await this.api.post<Device>('/api/v1/devices/register', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updatePushToken(pushToken: string): Promise<Device> {
|
||||
const response = await this.api.patch<Device>('/api/v1/devices/push-token', {
|
||||
pushToken,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getDevices(): Promise<DeviceListResponse> {
|
||||
const response = await this.api.get<DeviceListResponse>('/api/v1/devices');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getDevice(deviceId: string): Promise<Device> {
|
||||
const response = await this.api.get<Device>(`/api/v1/devices/${deviceId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteDevice(deviceId: string): Promise<void> {
|
||||
await this.api.delete(`/api/v1/devices/${deviceId}`);
|
||||
}
|
||||
|
||||
async getCurrentDevice(): Promise<Device | null> {
|
||||
try {
|
||||
const response = await this.api.get<Device>('/api/v1/devices/current');
|
||||
return response.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deviceService = new DeviceService();
|
||||
53
packages/mobile-api-client/src/api/notification.service.ts
Normal file
53
packages/mobile-api-client/src/api/notification.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Notification API service
|
||||
*/
|
||||
|
||||
import { getApiClient } from './api-client';
|
||||
import type { Notification, NotificationListResponse, NotificationPreferences } from '../types';
|
||||
|
||||
export class NotificationService {
|
||||
private api = getApiClient();
|
||||
|
||||
async getNotifications(params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
unreadOnly?: boolean;
|
||||
}): Promise<NotificationListResponse> {
|
||||
const response = await this.api.get<NotificationListResponse>('/notifications', { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getNotification(notificationId: string): Promise<Notification> {
|
||||
const response = await this.api.get<Notification>(`/notifications/${notificationId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async markAsRead(notificationId: string): Promise<void> {
|
||||
await this.api.patch(`/notifications/${notificationId}/read`);
|
||||
}
|
||||
|
||||
async markAllAsRead(): Promise<void> {
|
||||
await this.api.post('/notifications/read-all');
|
||||
}
|
||||
|
||||
async deleteNotification(notificationId: string): Promise<void> {
|
||||
await this.api.delete(`/notifications/${notificationId}`);
|
||||
}
|
||||
|
||||
async getPreferences(): Promise<NotificationPreferences> {
|
||||
const response = await this.api.get<NotificationPreferences>('/notifications/preferences');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updatePreferences(preferences: NotificationPreferences): Promise<NotificationPreferences> {
|
||||
const response = await this.api.put<NotificationPreferences>('/notifications/preferences', preferences);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getUnreadCount(): Promise<number> {
|
||||
const response = await this.api.get<{ count: number }>('/notifications/unread-count');
|
||||
return response.data.count;
|
||||
}
|
||||
}
|
||||
|
||||
export const notificationService = new NotificationService();
|
||||
53
packages/mobile-api-client/src/api/subscription.service.ts
Normal file
53
packages/mobile-api-client/src/api/subscription.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Subscription API service
|
||||
*/
|
||||
|
||||
import { getApiClient } from './api-client';
|
||||
import type {
|
||||
Subscription,
|
||||
SubscriptionTier,
|
||||
SubscriptionStatusResponse,
|
||||
CreateSubscriptionRequest,
|
||||
UpdateSubscriptionRequest,
|
||||
} from '../types';
|
||||
|
||||
export class SubscriptionService {
|
||||
private api = getApiClient();
|
||||
|
||||
async getSubscription(): Promise<SubscriptionStatusResponse> {
|
||||
const response = await this.api.get<SubscriptionStatusResponse>('/billing/subscription');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createSubscription(data: CreateSubscriptionRequest): Promise<Subscription> {
|
||||
const response = await this.api.post<Subscription>('/billing/subscription', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateSubscription(data: UpdateSubscriptionRequest): Promise<Subscription> {
|
||||
const response = await this.api.patch<Subscription>('/billing/subscription', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async cancelSubscription(): Promise<Subscription> {
|
||||
const response = await this.api.delete<Subscription>('/billing/subscription');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getTiers(): Promise<SubscriptionTier[]> {
|
||||
const response = await this.api.get<SubscriptionTier[]>('/billing/tiers');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createCheckoutSession(tier: string): Promise<{ url: string }> {
|
||||
const response = await this.api.post<{ url: string }>('/billing/checkout', { tier });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createCustomerPortalSession(): Promise<{ url: string }> {
|
||||
const response = await this.api.post<{ url: string }>('/billing/customer-portal');
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const subscriptionService = new SubscriptionService();
|
||||
53
packages/mobile-api-client/src/index.ts
Normal file
53
packages/mobile-api-client/src/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* ShieldAI Mobile API Client
|
||||
*
|
||||
* A comprehensive TypeScript API client library for React Native apps
|
||||
* to interact with ShieldAI backend services.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createApiClient, authService, deviceService } from '@shieldai/mobile-api-client';
|
||||
*
|
||||
* // Initialize the client
|
||||
* createApiClient({
|
||||
* baseURL: 'https://api.shieldai.freno.me/api/v1',
|
||||
* timeout: 30000,
|
||||
* debug: __DEV__,
|
||||
* });
|
||||
*
|
||||
* // Login
|
||||
* const { user, tokens } = await authService.login({
|
||||
* email: 'user@example.com',
|
||||
* password: 'password123',
|
||||
* });
|
||||
*
|
||||
* // Register device for push notifications
|
||||
* await deviceService.registerDevice({
|
||||
* platform: 'ios',
|
||||
* pushToken: '...',
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Core API client
|
||||
export {
|
||||
createApiClient,
|
||||
getApiClient,
|
||||
ApiClient,
|
||||
ApiClientConfig,
|
||||
} from './api/api-client';
|
||||
|
||||
// Services
|
||||
export { authService, AuthService } from './api/auth.service';
|
||||
export { deviceService, DeviceService } from './api/device.service';
|
||||
export { subscriptionService, SubscriptionService } from './api/subscription.service';
|
||||
export { notificationService, NotificationService } from './api/notification.service';
|
||||
|
||||
// Types
|
||||
export * from './types';
|
||||
|
||||
// Storage
|
||||
export { storage, tokenStorage, StorageAdapter } from './storage/token-storage';
|
||||
|
||||
// Utils
|
||||
export { requestQueue, RequestQueue } from './utils/request-queue';
|
||||
21
packages/mobile-api-client/src/react-native.d.ts
vendored
Normal file
21
packages/mobile-api-client/src/react-native.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
// Type declarations for React Native and Expo packages
|
||||
declare module 'expo-secure-store' {
|
||||
export function getItemAsync(key: string): Promise<string | null>;
|
||||
export function setItemAsync(key: string, value: string): Promise<void>;
|
||||
export function deleteItemAsync(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
declare module '@react-native-async-storage/async-storage' {
|
||||
export function getItem(key: string): Promise<string | null>;
|
||||
export function setItem(key: string, value: string): Promise<void>;
|
||||
export function removeItem(key: string): Promise<void>;
|
||||
export function clear(): Promise<void>;
|
||||
}
|
||||
|
||||
declare module 'react-native' {
|
||||
export import Platform = require('react-native/Libraries/Utilities/Platform');
|
||||
export const Platform: {
|
||||
OS: 'ios' | 'android' | 'web' | 'windows' | 'macos';
|
||||
Version: number;
|
||||
};
|
||||
}
|
||||
93
packages/mobile-api-client/src/storage/token-storage.ts
Normal file
93
packages/mobile-api-client/src/storage/token-storage.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Secure storage for authentication tokens
|
||||
* Uses expo-secure-store for production, AsyncStorage for fallback
|
||||
*/
|
||||
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
const ACCESS_TOKEN_KEY = '@shieldai:access_token';
|
||||
const REFRESH_TOKEN_KEY = '@shieldai:refresh_token';
|
||||
|
||||
export interface StorageAdapter {
|
||||
getItem: (key: string) => Promise<string | null>;
|
||||
setItem: (key: string, value: string) => Promise<void>;
|
||||
removeItem: (key: string) => Promise<void>;
|
||||
}
|
||||
|
||||
class SecureStorageAdapter implements StorageAdapter {
|
||||
async getItem(key: string): Promise<string | null> {
|
||||
try {
|
||||
return await SecureStore.getItemAsync(key);
|
||||
} catch {
|
||||
// Fallback to AsyncStorage if SecureStore fails
|
||||
return await AsyncStorage.getItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
async setItem(key: string, value: string): Promise<void> {
|
||||
try {
|
||||
await SecureStore.setItemAsync(key, value);
|
||||
} catch {
|
||||
await AsyncStorage.setItem(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
async removeItem(key: string): Promise<void> {
|
||||
try {
|
||||
await SecureStore.deleteItemAsync(key);
|
||||
} catch {
|
||||
await AsyncStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryStorageAdapter implements StorageAdapter {
|
||||
private store: Map<string, string> = new Map();
|
||||
|
||||
async getItem(key: string): Promise<string | null> {
|
||||
return this.store.get(key) || null;
|
||||
}
|
||||
|
||||
async setItem(key: string, value: string): Promise<void> {
|
||||
this.store.set(key, value);
|
||||
}
|
||||
|
||||
async removeItem(key: string): Promise<void> {
|
||||
this.store.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect environment and choose appropriate storage
|
||||
const getStorageAdapter = (): StorageAdapter => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return new InMemoryStorageAdapter();
|
||||
}
|
||||
return new SecureStorageAdapter();
|
||||
};
|
||||
|
||||
export const storage = getStorageAdapter();
|
||||
|
||||
export const tokenStorage = {
|
||||
async getAccessToken(): Promise<string | null> {
|
||||
return await storage.getItem(ACCESS_TOKEN_KEY);
|
||||
},
|
||||
|
||||
async getRefreshToken(): Promise<string | null> {
|
||||
return await storage.getItem(REFRESH_TOKEN_KEY);
|
||||
},
|
||||
|
||||
async saveTokens(accessToken: string, refreshToken: string): Promise<void> {
|
||||
await Promise.all([
|
||||
storage.setItem(ACCESS_TOKEN_KEY, accessToken),
|
||||
storage.setItem(REFRESH_TOKEN_KEY, refreshToken),
|
||||
]);
|
||||
},
|
||||
|
||||
async clearTokens(): Promise<void> {
|
||||
await Promise.all([
|
||||
storage.removeItem(ACCESS_TOKEN_KEY),
|
||||
storage.removeItem(REFRESH_TOKEN_KEY),
|
||||
]);
|
||||
},
|
||||
};
|
||||
46
packages/mobile-api-client/src/types/auth.types.ts
Normal file
46
packages/mobile-api-client/src/types/auth.types.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Authentication types for ShieldAI API
|
||||
*/
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AuthTokens {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
tokenType: 'Bearer';
|
||||
}
|
||||
|
||||
export interface LoginCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterData {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
user: User;
|
||||
tokens: AuthTokens;
|
||||
}
|
||||
|
||||
export interface RefreshTokenRequest {
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export interface AuthError {
|
||||
code: string;
|
||||
message: string;
|
||||
statusCode: number;
|
||||
}
|
||||
37
packages/mobile-api-client/src/types/common.types.ts
Normal file
37
packages/mobile-api-client/src/types/common.types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Shared API types
|
||||
*/
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: Record<string, string[]>;
|
||||
statusCode: number;
|
||||
}
|
||||
|
||||
export interface HealthStatus {
|
||||
status: 'healthy' | 'degraded' | 'unhealthy';
|
||||
timestamp: string;
|
||||
version: string;
|
||||
services?: Record<string, { status: 'healthy' | 'degraded' | 'unhealthy' }>;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
version: string;
|
||||
environment: string;
|
||||
build: string;
|
||||
}
|
||||
30
packages/mobile-api-client/src/types/device.types.ts
Normal file
30
packages/mobile-api-client/src/types/device.types.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Device types for push notification and device management
|
||||
*/
|
||||
|
||||
export interface Device {
|
||||
id: string;
|
||||
userId: string;
|
||||
platform: 'ios' | 'android';
|
||||
pushToken?: string;
|
||||
modelName?: string;
|
||||
osVersion?: string;
|
||||
appVersion?: string;
|
||||
isActive: boolean;
|
||||
lastActiveAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface DeviceRegistration {
|
||||
platform: 'ios' | 'android';
|
||||
pushToken: string;
|
||||
modelName?: string;
|
||||
osVersion?: string;
|
||||
appVersion?: string;
|
||||
}
|
||||
|
||||
export interface DeviceListResponse {
|
||||
devices: Device[];
|
||||
total: number;
|
||||
}
|
||||
5
packages/mobile-api-client/src/types/index.ts
Normal file
5
packages/mobile-api-client/src/types/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './auth.types';
|
||||
export * from './device.types';
|
||||
export * from './subscription.types';
|
||||
export * from './notification.types';
|
||||
export * from './common.types';
|
||||
27
packages/mobile-api-client/src/types/notification.types.ts
Normal file
27
packages/mobile-api-client/src/types/notification.types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Notification types
|
||||
*/
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
userId: string;
|
||||
type: 'darkwatch_alert' | 'spam_blocked' | 'voiceprint_analysis' | 'subscription' | 'system';
|
||||
title: string;
|
||||
message: string;
|
||||
data?: Record<string, unknown>;
|
||||
isRead: boolean;
|
||||
createdAt: string;
|
||||
readAt?: string;
|
||||
}
|
||||
|
||||
export interface NotificationListResponse {
|
||||
notifications: Notification[];
|
||||
total: number;
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
export interface NotificationPreferences {
|
||||
emailNotifications: boolean;
|
||||
pushNotifications: boolean;
|
||||
notificationTypes: Record<string, boolean>;
|
||||
}
|
||||
49
packages/mobile-api-client/src/types/subscription.types.ts
Normal file
49
packages/mobile-api-client/src/types/subscription.types.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Subscription and billing types
|
||||
*/
|
||||
|
||||
export interface Subscription {
|
||||
id: string;
|
||||
userId: string;
|
||||
tier: 'free' | 'basic' | 'premium' | 'enterprise';
|
||||
status: 'active' | 'canceled' | 'past_due' | 'trialing';
|
||||
stripeCustomerId: string;
|
||||
stripeSubscriptionId?: string;
|
||||
currentPeriodStart: string;
|
||||
currentPeriodEnd: string;
|
||||
cancelAtPeriodEnd: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface SubscriptionTier {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
currency: string;
|
||||
interval: 'month' | 'year';
|
||||
features: string[];
|
||||
}
|
||||
|
||||
export interface CreateSubscriptionRequest {
|
||||
tier: 'free' | 'basic' | 'premium' | 'enterprise';
|
||||
paymentMethodId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateSubscriptionRequest {
|
||||
tier?: 'free' | 'basic' | 'premium' | 'enterprise';
|
||||
cancelAtPeriodEnd?: boolean;
|
||||
}
|
||||
|
||||
export interface SubscriptionStatusResponse {
|
||||
subscription: Subscription;
|
||||
tier: SubscriptionTier;
|
||||
usage: {
|
||||
currentPeriod: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
features: Record<string, { used: number; limit: number | null }>;
|
||||
};
|
||||
}
|
||||
141
packages/mobile-api-client/src/utils/request-queue.ts
Normal file
141
packages/mobile-api-client/src/utils/request-queue.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Request queue for offline support
|
||||
* Queues API requests when offline and replays when online
|
||||
*/
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
const QUEUE_KEY = '@shieldai:api_queue';
|
||||
const MAX_QUEUE_SIZE = 100;
|
||||
|
||||
export interface QueuedRequest {
|
||||
id: string;
|
||||
config: AxiosRequestConfig;
|
||||
timestamp: number;
|
||||
retryCount: number;
|
||||
maxRetries: number;
|
||||
}
|
||||
|
||||
export interface QueueStatus {
|
||||
size: number;
|
||||
oldestRequest: number | null;
|
||||
newestRequest: number | null;
|
||||
}
|
||||
|
||||
export class RequestQueue {
|
||||
private queue: QueuedRequest[] = [];
|
||||
private isProcessing = false;
|
||||
private listeners: Set<() => void> = new Set();
|
||||
|
||||
constructor() {
|
||||
this.loadFromStorage();
|
||||
}
|
||||
|
||||
private notifyListeners(): void {
|
||||
this.listeners.forEach((listener) => listener());
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
async loadFromStorage(): Promise<void> {
|
||||
try {
|
||||
const data = await AsyncStorage.getItem(QUEUE_KEY);
|
||||
if (data) {
|
||||
this.queue = JSON.parse(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load request queue:', error);
|
||||
this.queue = [];
|
||||
}
|
||||
}
|
||||
|
||||
private async saveToStorage(): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(this.queue));
|
||||
} catch (error) {
|
||||
console.error('Failed to save request queue:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async enqueue(config: AxiosRequestConfig): Promise<string> {
|
||||
const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const queuedRequest: QueuedRequest = {
|
||||
id,
|
||||
config,
|
||||
timestamp: Date.now(),
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
};
|
||||
|
||||
// Limit queue size
|
||||
if (this.queue.length >= MAX_QUEUE_SIZE) {
|
||||
this.queue.shift(); // Remove oldest
|
||||
}
|
||||
|
||||
this.queue.push(queuedRequest);
|
||||
await this.saveToStorage();
|
||||
this.notifyListeners();
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async dequeue(): Promise<QueuedRequest | null> {
|
||||
if (this.queue.length === 0) return null;
|
||||
|
||||
const request = this.queue.shift();
|
||||
if (request) {
|
||||
await this.saveToStorage();
|
||||
this.notifyListeners();
|
||||
}
|
||||
return request ?? null;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const index = this.queue.findIndex((r) => r.id === id);
|
||||
if (index !== -1) {
|
||||
this.queue.splice(index, 1);
|
||||
await this.saveToStorage();
|
||||
this.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
async retry(id: string): Promise<void> {
|
||||
const index = this.queue.findIndex((r) => r.id === id);
|
||||
if (index !== -1) {
|
||||
this.queue[index].retryCount += 1;
|
||||
await this.saveToStorage();
|
||||
this.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.queue = [];
|
||||
await AsyncStorage.removeItem(QUEUE_KEY);
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
getStatus(): QueueStatus {
|
||||
if (this.queue.length === 0) {
|
||||
return { size: 0, oldestRequest: null, newestRequest: null };
|
||||
}
|
||||
|
||||
return {
|
||||
size: this.queue.length,
|
||||
oldestRequest: this.queue[0]?.timestamp ?? null,
|
||||
newestRequest: this.queue[this.queue.length - 1]?.timestamp ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
isOnline(): boolean {
|
||||
// In React Native, you'd use NetInfo here
|
||||
// For now, return true (assume online)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const requestQueue = new RequestQueue();
|
||||
Reference in New Issue
Block a user