mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-18 06:17:21 +00:00
Prepare codebase for future cross-platform deployment
Implement platform abstraction layer for API requests and storage, and document multi-platform strategy. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 279f1558-c0e3-40e4-8217-be7e9f4c6eca Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 4ad7a49d-0f69-4e30-a6ae-edccda64bd96 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/b984cb14-1d19-4944-922b-bc79e821ed35/279f1558-c0e3-40e4-8217-be7e9f4c6eca/jIK7HfC Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
385e5e8a04
commit
89c576aac7
4 changed files with 298 additions and 1 deletions
85
client/src/lib/api.ts
Normal file
85
client/src/lib/api.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { platformConfig } from './platform';
|
||||
|
||||
type RequestOptions = Omit<RequestInit, 'body'> & {
|
||||
body?: Record<string, unknown> | FormData;
|
||||
};
|
||||
|
||||
export async function apiRequest(
|
||||
endpoint: string,
|
||||
options: RequestOptions = {}
|
||||
): Promise<Response> {
|
||||
const { body, headers, ...rest } = options;
|
||||
|
||||
const url = `${platformConfig.apiBaseUrl}${endpoint}`;
|
||||
|
||||
const requestHeaders: Record<string, string> = {};
|
||||
|
||||
if (headers) {
|
||||
if (headers instanceof Headers) {
|
||||
headers.forEach((value, key) => {
|
||||
requestHeaders[key] = value;
|
||||
});
|
||||
} else if (Array.isArray(headers)) {
|
||||
headers.forEach(([key, value]) => {
|
||||
requestHeaders[key] = value;
|
||||
});
|
||||
} else {
|
||||
Object.assign(requestHeaders, headers);
|
||||
}
|
||||
}
|
||||
|
||||
if (body && !(body instanceof FormData)) {
|
||||
requestHeaders['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...rest,
|
||||
headers: requestHeaders,
|
||||
credentials: 'include',
|
||||
body: body instanceof FormData ? body : body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function apiGet<T>(endpoint: string): Promise<T> {
|
||||
const response = await apiRequest(endpoint, { method: 'GET' });
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Request failed' }));
|
||||
throw new Error(error.error || 'Request failed');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function apiPost<T>(endpoint: string, data?: Record<string, unknown>): Promise<T> {
|
||||
const response = await apiRequest(endpoint, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Request failed' }));
|
||||
throw new Error(error.error || 'Request failed');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function apiPut<T>(endpoint: string, data?: Record<string, unknown>): Promise<T> {
|
||||
const response = await apiRequest(endpoint, {
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Request failed' }));
|
||||
throw new Error(error.error || 'Request failed');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function apiDelete<T>(endpoint: string): Promise<T> {
|
||||
const response = await apiRequest(endpoint, { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Request failed' }));
|
||||
throw new Error(error.error || 'Request failed');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
92
client/src/lib/platform.ts
Normal file
92
client/src/lib/platform.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
export type PlatformType = 'web' | 'desktop' | 'mobile';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI__?: unknown;
|
||||
flutter_inappwebview?: unknown;
|
||||
Capacitor?: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
interface PlatformConfig {
|
||||
platform: PlatformType;
|
||||
apiBaseUrl: string;
|
||||
isSecureContext: boolean;
|
||||
supportsNotifications: boolean;
|
||||
supportsFileSystem: boolean;
|
||||
}
|
||||
|
||||
let cachedPlatform: PlatformType | null = null;
|
||||
|
||||
function detectPlatform(): PlatformType {
|
||||
if (cachedPlatform !== null) return cachedPlatform;
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
cachedPlatform = 'web';
|
||||
return cachedPlatform;
|
||||
}
|
||||
|
||||
if (window.__TAURI__ !== undefined) {
|
||||
cachedPlatform = 'desktop';
|
||||
return cachedPlatform;
|
||||
}
|
||||
|
||||
if (window.flutter_inappwebview !== undefined || window.Capacitor !== undefined) {
|
||||
cachedPlatform = 'mobile';
|
||||
return cachedPlatform;
|
||||
}
|
||||
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
|
||||
if (userAgent.includes('electron')) {
|
||||
cachedPlatform = 'desktop';
|
||||
return cachedPlatform;
|
||||
}
|
||||
|
||||
if (userAgent.includes('cordova')) {
|
||||
cachedPlatform = 'mobile';
|
||||
return cachedPlatform;
|
||||
}
|
||||
|
||||
cachedPlatform = 'web';
|
||||
return cachedPlatform;
|
||||
}
|
||||
|
||||
function getApiBaseUrl(): string {
|
||||
const platform = detectPlatform();
|
||||
|
||||
if (platform === 'web') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const envUrl = import.meta.env.VITE_API_BASE_URL;
|
||||
if (envUrl) return envUrl;
|
||||
|
||||
return 'https://aethex.network';
|
||||
}
|
||||
|
||||
export function getPlatformConfig(): PlatformConfig {
|
||||
const platform = detectPlatform();
|
||||
|
||||
return {
|
||||
platform,
|
||||
apiBaseUrl: getApiBaseUrl(),
|
||||
isSecureContext: typeof window !== 'undefined' && window.isSecureContext,
|
||||
supportsNotifications: typeof Notification !== 'undefined',
|
||||
supportsFileSystem: typeof window !== 'undefined' && 'showOpenFilePicker' in window,
|
||||
};
|
||||
}
|
||||
|
||||
export function isDesktop(): boolean {
|
||||
return detectPlatform() === 'desktop';
|
||||
}
|
||||
|
||||
export function isMobile(): boolean {
|
||||
return detectPlatform() === 'mobile';
|
||||
}
|
||||
|
||||
export function isWeb(): boolean {
|
||||
return detectPlatform() === 'web';
|
||||
}
|
||||
|
||||
export const platformConfig = getPlatformConfig();
|
||||
91
client/src/lib/storage.ts
Normal file
91
client/src/lib/storage.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { isDesktop } from './platform';
|
||||
|
||||
export interface StorageAdapter {
|
||||
get(key: string): Promise<string | null>;
|
||||
set(key: string, value: string): Promise<void>;
|
||||
remove(key: string): Promise<void>;
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
|
||||
class BrowserStorageAdapter implements StorageAdapter {
|
||||
async get(key: string): Promise<string | null> {
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
async set(key: string, value: string): Promise<void> {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<void> {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
localStorage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
interface TauriAPI {
|
||||
core: {
|
||||
invoke: <T>(cmd: string, args?: Record<string, unknown>) => Promise<T>;
|
||||
};
|
||||
}
|
||||
|
||||
function getTauriAPI(): TauriAPI | null {
|
||||
if (typeof window !== 'undefined' && window.__TAURI__ !== undefined) {
|
||||
const tauri = window.__TAURI__ as TauriAPI;
|
||||
if (tauri?.core?.invoke) {
|
||||
return tauri;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class SecureStorageAdapter implements StorageAdapter {
|
||||
private async tauriInvoke<T>(cmd: string, args?: Record<string, unknown>): Promise<T | null> {
|
||||
const tauri = getTauriAPI();
|
||||
if (tauri) {
|
||||
try {
|
||||
return await tauri.core.invoke<T>(cmd, args);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | null> {
|
||||
const result = await this.tauriInvoke<string>('get_secure_value', { key });
|
||||
if (result !== null) return result;
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
async set(key: string, value: string): Promise<void> {
|
||||
const result = await this.tauriInvoke<void>('set_secure_value', { key, value });
|
||||
if (result === undefined && typeof window !== 'undefined' && window.__TAURI__) return;
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<void> {
|
||||
const result = await this.tauriInvoke<void>('remove_secure_value', { key });
|
||||
if (result === undefined && typeof window !== 'undefined' && window.__TAURI__) return;
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
localStorage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
function createStorageAdapter(): StorageAdapter {
|
||||
if (isDesktop()) {
|
||||
return new SecureStorageAdapter();
|
||||
}
|
||||
return new BrowserStorageAdapter();
|
||||
}
|
||||
|
||||
export const storage = createStorageAdapter();
|
||||
|
||||
export function useStorage() {
|
||||
return storage;
|
||||
}
|
||||
31
replit.md
31
replit.md
|
|
@ -98,4 +98,33 @@ Preferred communication style: Simple, everyday language.
|
|||
### Development Tools
|
||||
- Vite development server with HMR
|
||||
- Replit-specific plugins for development (cartographer, dev-banner, error overlay)
|
||||
- TypeScript with strict mode enabled
|
||||
- TypeScript with strict mode enabled
|
||||
|
||||
## Multi-Platform Strategy (Q3 2025 Roadmap)
|
||||
|
||||
### Current State: Web-First
|
||||
The AeThex OS (`/os` route) is currently a web application. The codebase has been prepared for future multi-platform deployment with abstraction layers.
|
||||
|
||||
### Platform Abstraction Layer
|
||||
Located in `client/src/lib/`:
|
||||
- **`platform.ts`**: Detects runtime environment (web, desktop, mobile) and provides platform-specific configuration
|
||||
- **`storage.ts`**: Abstract storage adapter that uses localStorage for web and can use secure storage (keychain) for desktop/mobile
|
||||
- **`api.ts`**: Centralized API request layer with configurable base URLs for different deployment contexts
|
||||
|
||||
### Future: Flutter Desktop App (Q3 2025)
|
||||
**Why Flutter over Tauri/Electron:**
|
||||
1. **Custom Rendering**: Skia/Impeller engine draws every pixel - perfect for the cyberpunk/Aegis Terminal aesthetic
|
||||
2. **Cross-Platform Code Sharing**: Same codebase for iOS, Android, Windows, macOS
|
||||
3. **Native Performance**: 60/120 FPS custom animations without browser overhead
|
||||
4. **Passport Use Case**: Ideal for secure "Wallet/Authenticator" style apps
|
||||
|
||||
**Migration Path:**
|
||||
1. Web remains the primary platform until revenue milestone ($50k+)
|
||||
2. Flutter app will consume the same backend API (hosted on aethex.network)
|
||||
3. Desktop builds will use secure storage for Supabase tokens
|
||||
4. Mobile app serves as "Aegis Companion" - authenticator/passport viewer, not game client
|
||||
|
||||
### Environment Configuration
|
||||
For desktop/mobile builds, set:
|
||||
- `VITE_API_BASE_URL`: Points to production API (https://aethex.network)
|
||||
- Platform detection automatically adjusts storage and API handling
|
||||
Loading…
Reference in a new issue