From 89c576aac7e9f6013564762e233cbc75df11760d Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Wed, 17 Dec 2025 04:59:41 +0000 Subject: [PATCH] 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 --- client/src/lib/api.ts | 85 +++++++++++++++++++++++++++++++++++ client/src/lib/platform.ts | 92 ++++++++++++++++++++++++++++++++++++++ client/src/lib/storage.ts | 91 +++++++++++++++++++++++++++++++++++++ replit.md | 31 ++++++++++++- 4 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 client/src/lib/api.ts create mode 100644 client/src/lib/platform.ts create mode 100644 client/src/lib/storage.ts diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts new file mode 100644 index 0000000..4cdbf97 --- /dev/null +++ b/client/src/lib/api.ts @@ -0,0 +1,85 @@ +import { platformConfig } from './platform'; + +type RequestOptions = Omit & { + body?: Record | FormData; +}; + +export async function apiRequest( + endpoint: string, + options: RequestOptions = {} +): Promise { + const { body, headers, ...rest } = options; + + const url = `${platformConfig.apiBaseUrl}${endpoint}`; + + const requestHeaders: Record = {}; + + 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(endpoint: string): Promise { + 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(endpoint: string, data?: Record): Promise { + 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(endpoint: string, data?: Record): Promise { + 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(endpoint: string): Promise { + 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(); +} diff --git a/client/src/lib/platform.ts b/client/src/lib/platform.ts new file mode 100644 index 0000000..baf4e2a --- /dev/null +++ b/client/src/lib/platform.ts @@ -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(); diff --git a/client/src/lib/storage.ts b/client/src/lib/storage.ts new file mode 100644 index 0000000..58b59d6 --- /dev/null +++ b/client/src/lib/storage.ts @@ -0,0 +1,91 @@ +import { isDesktop } from './platform'; + +export interface StorageAdapter { + get(key: string): Promise; + set(key: string, value: string): Promise; + remove(key: string): Promise; + clear(): Promise; +} + +class BrowserStorageAdapter implements StorageAdapter { + async get(key: string): Promise { + return localStorage.getItem(key); + } + + async set(key: string, value: string): Promise { + localStorage.setItem(key, value); + } + + async remove(key: string): Promise { + localStorage.removeItem(key); + } + + async clear(): Promise { + localStorage.clear(); + } +} + +interface TauriAPI { + core: { + invoke: (cmd: string, args?: Record) => Promise; + }; +} + +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(cmd: string, args?: Record): Promise { + const tauri = getTauriAPI(); + if (tauri) { + try { + return await tauri.core.invoke(cmd, args); + } catch { + return null; + } + } + return null; + } + + async get(key: string): Promise { + const result = await this.tauriInvoke('get_secure_value', { key }); + if (result !== null) return result; + return localStorage.getItem(key); + } + + async set(key: string, value: string): Promise { + const result = await this.tauriInvoke('set_secure_value', { key, value }); + if (result === undefined && typeof window !== 'undefined' && window.__TAURI__) return; + localStorage.setItem(key, value); + } + + async remove(key: string): Promise { + const result = await this.tauriInvoke('remove_secure_value', { key }); + if (result === undefined && typeof window !== 'undefined' && window.__TAURI__) return; + localStorage.removeItem(key); + } + + async clear(): Promise { + localStorage.clear(); + } +} + +function createStorageAdapter(): StorageAdapter { + if (isDesktop()) { + return new SecureStorageAdapter(); + } + return new BrowserStorageAdapter(); +} + +export const storage = createStorageAdapter(); + +export function useStorage() { + return storage; +} diff --git a/replit.md b/replit.md index 3c5f551..36256f1 100644 --- a/replit.md +++ b/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 \ No newline at end of file +- 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 \ No newline at end of file