diff --git a/android-app/capacitor-bridge.js b/android-app/capacitor-bridge.js index 72048ed..9fae015 100644 --- a/android-app/capacitor-bridge.js +++ b/android-app/capacitor-bridge.js @@ -1,4 +1,15 @@ -const BACKEND_BASE_URL = window.BACKEND_BASE_URL || 'https://api.example.com'; +// Capacitor Bridge for Plugin Compass Mobile App +// Connects to backend at plugincompass.com:9445 + +const BACKEND_BASE_URL = window.BACKEND_BASE_URL || 'https://plugincompass.com:9445'; + +// Storage keys +const STORAGE_KEYS = { + USER: 'plugin_compass_user', + TOKEN: 'plugin_compass_token', + REFRESH_TOKEN: 'plugin_compass_refresh_token', + SESSION: 'plugin_compass_session' +}; let PreferencesImpl = null; @@ -58,31 +69,378 @@ export const Preferences = { } }; +// Token management +async function getAuthToken() { + try { + const { value } = await Preferences.get({ key: STORAGE_KEYS.TOKEN }); + return value; + } catch (e) { + console.error('[AUTH] Failed to get token:', e); + return null; + } +} + +async function setAuthToken(token) { + try { + await Preferences.set({ key: STORAGE_KEYS.TOKEN, value: token }); + } catch (e) { + console.error('[AUTH] Failed to set token:', e); + } +} + +async function getRefreshToken() { + try { + const { value } = await Preferences.get({ key: STORAGE_KEYS.REFRESH_TOKEN }); + return value; + } catch (e) { + return null; + } +} + +async function setRefreshToken(token) { + try { + await Preferences.set({ key: STORAGE_KEYS.REFRESH_TOKEN, value: token }); + } catch (e) { + console.error('[AUTH] Failed to set refresh token:', e); + } +} + +async function clearAuth() { + try { + await Preferences.remove({ key: STORAGE_KEYS.TOKEN }); + await Preferences.remove({ key: STORAGE_KEYS.REFRESH_TOKEN }); + await Preferences.remove({ key: STORAGE_KEYS.USER }); + await Preferences.remove({ key: STORAGE_KEYS.SESSION }); + } catch (e) { + console.error('[AUTH] Failed to clear auth:', e); + } +} + +// API client with authentication +async function apiClient(endpoint, options = {}) { + const url = endpoint.startsWith('http') ? endpoint : `${BACKEND_BASE_URL}${endpoint}`; + + const token = await getAuthToken(); + const headers = { + 'Content-Type': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}), + ...(options.headers || {}) + }; + + try { + const response = await fetch(url, { + ...options, + headers, + credentials: 'include' + }); + + // Handle token expiration + if (response.status === 401) { + const refreshed = await refreshAccessToken(); + if (refreshed) { + // Retry with new token + const newToken = await getAuthToken(); + headers['Authorization'] = `Bearer ${newToken}`; + const retryResponse = await fetch(url, { + ...options, + headers, + credentials: 'include' + }); + return handleResponse(retryResponse); + } else { + // Token refresh failed, clear auth + await clearAuth(); + throw new Error('Session expired. Please sign in again.'); + } + } + + return handleResponse(response); + } catch (error) { + console.error(`[API] Request failed: ${endpoint}`, error); + throw error; + } +} + +async function handleResponse(response) { + const text = await response.text(); + let json; + try { + json = text ? JSON.parse(text) : {}; + } catch (parseErr) { + throw new Error(`Invalid JSON response: ${parseErr.message}`); + } + + if (!response.ok) { + const err = new Error(json.error || json.message || `HTTP ${response.status}`); + err.status = response.status; + err.data = json; + throw err; + } + + return json; +} + +async function refreshAccessToken() { + try { + const refreshToken = await getRefreshToken(); + if (!refreshToken) return false; + + const response = await fetch(`${BACKEND_BASE_URL}/api/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken }) + }); + + if (!response.ok) { + return false; + } + + const data = await response.json(); + if (data.token) { + await setAuthToken(data.token); + if (data.refreshToken) { + await setRefreshToken(data.refreshToken); + } + return true; + } + return false; + } catch (e) { + console.error('[AUTH] Token refresh failed:', e); + return false; + } +} + +// Authentication functions +export async function loginWithEmail(email, password, remember = false) { + try { + const response = await apiClient('/api/login', { + method: 'POST', + body: JSON.stringify({ email, password, remember }) + }); + + if (response.ok && response.user && response.token) { + // Store tokens + await setAuthToken(response.token); + if (response.refreshToken) { + await setRefreshToken(response.refreshToken); + } + + // Store user info + await Preferences.set({ + key: STORAGE_KEYS.USER, + value: JSON.stringify(response.user) + }); + + return { success: true, user: response.user }; + } else { + return { success: false, error: response.error || 'Login failed' }; + } + } catch (error) { + console.error('[AUTH] Login error:', error); + return { success: false, error: error.message || 'Login failed' }; + } +} + +export async function signupWithEmail(email, password) { + try { + const response = await apiClient('/api/signup', { + method: 'POST', + body: JSON.stringify({ email, password }) + }); + + if (response.ok && response.user && response.token) { + await setAuthToken(response.token); + if (response.refreshToken) { + await setRefreshToken(response.refreshToken); + } + + await Preferences.set({ + key: STORAGE_KEYS.USER, + value: JSON.stringify(response.user) + }); + + return { success: true, user: response.user }; + } else { + return { success: false, error: response.error || 'Signup failed' }; + } + } catch (error) { + console.error('[AUTH] Signup error:', error); + return { success: false, error: error.message || 'Signup failed' }; + } +} + +export async function logout() { + try { + // Call logout endpoint if available + await apiClient('/api/logout', { + method: 'POST', + headers: {} + }).catch(() => {}); + } finally { + await clearAuth(); + } +} + +export async function getCurrentUser() { + try { + // First check stored user + const { value } = await Preferences.get({ key: STORAGE_KEYS.USER }); + if (value) { + const user = JSON.parse(value); + + // Verify token is still valid by fetching fresh user data + try { + const freshUser = await apiClient('/api/me'); + if (freshUser) { + await Preferences.set({ + key: STORAGE_KEYS.USER, + value: JSON.stringify(freshUser) + }); + return freshUser; + } + } catch (e) { + console.warn('[AUTH] Could not refresh user data:', e.message); + } + + return user; + } + return null; + } catch (e) { + console.error('[AUTH] Failed to get current user:', e); + return null; + } +} + +export async function isAuthenticated() { + const token = await getAuthToken(); + if (!token) return false; + + const user = await getCurrentUser(); + return !!user; +} + +// OAuth helpers +export function getOAuthUrl(provider, redirectUri = null) { + const params = new URLSearchParams(); + if (redirectUri) { + params.set('redirect_uri', redirectUri); + } + params.set('mobile', 'true'); + return `${BACKEND_BASE_URL}/auth/${provider}?${params.toString()}`; +} + +export async function handleOAuthCallback(provider, code, state) { + try { + const response = await apiClient(`/auth/${provider}/callback`, { + method: 'POST', + body: JSON.stringify({ code, state }) + }); + + if (response.token) { + await setAuthToken(response.token); + if (response.refreshToken) { + await setRefreshToken(response.refreshToken); + } + + if (response.user) { + await Preferences.set({ + key: STORAGE_KEYS.USER, + value: JSON.stringify(response.user) + }); + } + + return { success: true, user: response.user }; + } else { + return { success: false, error: response.error || 'OAuth failed' }; + } + } catch (error) { + console.error('[AUTH] OAuth error:', error); + return { success: false, error: error.message || 'OAuth failed' }; + } +} + +// Plugin/App management +export async function getUserApps() { + try { + const apps = await apiClient('/api/apps'); + return { success: true, apps: apps || [] }; + } catch (error) { + console.error('[APPS] Failed to load apps:', error); + return { success: false, error: error.message, apps: [] }; + } +} + +export async function getApp(appId) { + try { + const app = await apiClient(`/api/apps/${appId}`); + return { success: true, app }; + } catch (error) { + console.error('[APPS] Failed to load app:', error); + return { success: false, error: error.message }; + } +} + +export async function createApp(appData) { + try { + const app = await apiClient('/api/apps', { + method: 'POST', + body: JSON.stringify(appData) + }); + return { success: true, app }; + } catch (error) { + console.error('[APPS] Failed to create app:', error); + return { success: false, error: error.message }; + } +} + +export async function updateApp(appId, appData) { + try { + const app = await apiClient(`/api/apps/${appId}`, { + method: 'PUT', + body: JSON.stringify(appData) + }); + return { success: true, app }; + } catch (error) { + console.error('[APPS] Failed to update app:', error); + return { success: false, error: error.message }; + } +} + +export async function deleteApp(appId) { + try { + await apiClient(`/api/apps/${appId}`, { + method: 'DELETE' + }); + return { success: true }; + } catch (error) { + console.error('[APPS] Failed to delete app:', error); + return { success: false, error: error.message }; + } +} + export async function syncApp(appId) { if (!appId || appId.trim() === '') { throw new Error('appId is required'); } - const { value } = await Preferences.get({ key: `app_${appId}` }); - if (!value) { - throw new Error(`App not found: ${appId}`); + try { + // Get app from backend instead of local storage + const result = await getApp(appId); + if (!result.success || !result.app) { + throw new Error(`App not found: ${appId}`); + } + + // Sync with backend + const response = await apiClient('/desktop/apps/sync', { + method: 'POST', + body: JSON.stringify(result.app) + }); + + return response; + } catch (error) { + console.error('[SYNC] Failed to sync app:', error); + throw error; } - - const appData = JSON.parse(value); - - const response = await fetch(`${BACKEND_BASE_URL}/desktop/apps/sync`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(appData), - }); - - if (!response.ok) { - throw new Error(`Sync failed: ${response.status}`); - } - - return response.json(); } export async function runOpencodeTask(appId, taskName, args = []) { @@ -96,8 +454,158 @@ export async function runOpencodeTask(appId, taskName, args = []) { throw new Error('OpenCode execution is not supported on mobile devices. Please use the desktop app for this feature.'); } +// Session management +export async function getSessions() { + try { + const sessions = await apiClient('/api/sessions'); + return { success: true, sessions: sessions || [] }; + } catch (error) { + console.error('[SESSIONS] Failed to load sessions:', error); + return { success: false, error: error.message, sessions: [] }; + } +} + +export async function getSession(sessionId) { + try { + const session = await apiClient(`/api/sessions/${sessionId}`); + return { success: true, session }; + } catch (error) { + console.error('[SESSIONS] Failed to load session:', error); + return { success: false, error: error.message }; + } +} + +export async function createSession(sessionData) { + try { + const session = await apiClient('/api/sessions', { + method: 'POST', + body: JSON.stringify(sessionData) + }); + return { success: true, session }; + } catch (error) { + console.error('[SESSIONS] Failed to create session:', error); + return { success: false, error: error.message }; + } +} + +export async function sendMessage(sessionId, messageData) { + try { + const result = await apiClient(`/api/sessions/${sessionId}/messages`, { + method: 'POST', + body: JSON.stringify(messageData) + }); + return { success: true, ...result }; + } catch (error) { + console.error('[MESSAGES] Failed to send message:', error); + return { success: false, error: error.message }; + } +} + +// User account +export async function getUserAccount() { + try { + const account = await apiClient('/api/account'); + return { success: true, account }; + } catch (error) { + console.error('[ACCOUNT] Failed to load account:', error); + return { success: false, error: error.message }; + } +} + +export async function updateUserAccount(accountData) { + try { + const account = await apiClient('/api/account', { + method: 'PUT', + body: JSON.stringify(accountData) + }); + return { success: true, account }; + } catch (error) { + console.error('[ACCOUNT] Failed to update account:', error); + return { success: false, error: error.message }; + } +} + +export async function getUsageSummary() { + try { + const usage = await apiClient('/api/account/usage'); + return { success: true, usage }; + } catch (error) { + console.error('[USAGE] Failed to load usage:', error); + return { success: false, error: error.message }; + } +} + +// Legacy exports for backward compatibility window.nativeBridge = { Preferences, syncApp, - runOpencodeTask + runOpencodeTask, + loginWithEmail, + signupWithEmail, + logout, + getCurrentUser, + isAuthenticated, + getUserApps, + getApp, + createApp, + updateApp, + deleteApp, + getSessions, + createSession, + sendMessage, + getOAuthUrl, + handleOAuthCallback, + getUserAccount, + updateUserAccount, + getUsageSummary, + apiClient +}; + +// Expose all functions globally for the web UI +Object.assign(window, { + loginWithEmail, + signupWithEmail, + logout, + getCurrentUser, + isAuthenticated, + getUserApps, + getApp, + createApp, + updateApp, + deleteApp, + getSessions, + getSession, + createSession, + sendMessage, + getOAuthUrl, + handleOAuthCallback, + getUserAccount, + updateUserAccount, + getUsageSummary, + apiClient +}); + +export { + loginWithEmail, + signupWithEmail, + logout, + getCurrentUser, + isAuthenticated, + getUserApps, + getApp, + createApp, + updateApp, + deleteApp, + getSessions, + getSession, + createSession, + sendMessage, + getOAuthUrl, + handleOAuthCallback, + getUserAccount, + updateUserAccount, + getUsageSummary, + apiClient, + BACKEND_BASE_URL, + STORAGE_KEYS }; diff --git a/android-app/package.json b/android-app/package.json index d7d8d49..c49b863 100644 --- a/android-app/package.json +++ b/android-app/package.json @@ -2,6 +2,7 @@ "name": "plugin-compass-android", "version": "0.1.0", "private": true, + "type": "module", "scripts": { "prepare-ui": "node ./scripts/sync-ui.js", "build": "npm run prepare-ui && npx cap sync android", diff --git a/android-app/scripts/sync-ui.js b/android-app/scripts/sync-ui.js index 526c715..55b1e30 100644 --- a/android-app/scripts/sync-ui.js +++ b/android-app/scripts/sync-ui.js @@ -50,6 +50,11 @@ async function injectMetaTags() { html = html.replace('', ' \n'); } + // Add capacitor-bridge.js before closing body tag + if (!html.includes('capacitor-bridge.js')) { + html = html.replace('
@@ -370,25 +437,18 @@ async function createMobileIndex() {