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:
sirpiglr 2025-12-17 04:59:41 +00:00
parent 385e5e8a04
commit 89c576aac7
4 changed files with 298 additions and 1 deletions

85
client/src/lib/api.ts Normal file
View 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();
}

View 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
View 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;
}

View file

@ -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