// Capacitor Bridge for Plugin Compass Mobile App // Connects to backend at plugincompass.com const BACKEND_BASE_URL = window.BACKEND_BASE_URL || 'https://plugincompass.com'; // 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; async function getPreferences() { if (PreferencesImpl) return PreferencesImpl; try { const module = await import('@capacitor/preferences'); PreferencesImpl = module.Preferences; return PreferencesImpl; } catch { // Fallback to localStorage PreferencesImpl = { async get({ key }) { return { value: localStorage.getItem(key) }; }, async set({ key, value }) { localStorage.setItem(key, value); return; }, async remove({ key }) { localStorage.removeItem(key); return; }, async keys() { return { keys: Object.keys(localStorage) }; }, async clear() { localStorage.clear(); return; } }; return PreferencesImpl; } } export const Preferences = { async get(options) { const pref = await getPreferences(); return pref.get(options); }, async set(options) { const pref = await getPreferences(); return pref.set(options); }, async remove(options) { const pref = await getPreferences(); return pref.remove(options); }, async keys() { const pref = await getPreferences(); return pref.keys(); }, async clear() { const pref = await getPreferences(); return pref.clear(); } }; // 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 and timeout const REQUEST_TIMEOUT = 10000; // 10 second timeout function fetchWithTimeout(url, options, timeout = REQUEST_TIMEOUT) { return Promise.race([ fetch(url, options), new Promise((_, reject) => setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout) ) ]); } async function apiClient(endpoint, options = {}) { const url = endpoint.startsWith('http') ? endpoint : `${BACKEND_BASE_URL}${endpoint}`; const requestId = Math.random().toString(36).substring(7); console.log(`[API:${requestId}] Starting request to ${endpoint}`); console.log(`[API:${requestId}] Full URL: ${url}`); const token = await getAuthToken(); const headers = { 'Content-Type': 'application/json', ...(token ? { 'Authorization': `Bearer ${token}` } : {}), ...(options.headers || {}) }; try { console.log(`[API:${requestId}] Sending ${options.method || 'GET'} request...`); const startTime = Date.now(); const response = await fetchWithTimeout(url, { ...options, headers, credentials: 'include' }); const duration = Date.now() - startTime; console.log(`[API:${requestId}] Response received in ${duration}ms - Status: ${response.status}`); // Handle token expiration if (response.status === 401) { console.log(`[API:${requestId}] Token expired, attempting refresh...`); const refreshed = await refreshAccessToken(); if (refreshed) { console.log(`[API:${requestId}] Token refreshed, retrying request...`); // Retry with new token const newToken = await getAuthToken(); headers['Authorization'] = `Bearer ${newToken}`; const retryResponse = await fetchWithTimeout(url, { ...options, headers, credentials: 'include' }); return handleResponse(retryResponse); } else { // Token refresh failed, clear auth console.error(`[API:${requestId}] Token refresh failed`); await clearAuth(); throw new Error('Session expired. Please sign in again.'); } } return handleResponse(response); } catch (error) { console.error(`[API:${requestId}] Request failed: ${endpoint}`, error); console.error(`[API:${requestId}] Error details:`, { message: error.message, name: error.name, stack: error.stack }); 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() { console.log('[AUTH] Getting current user...'); try { // First check stored user console.log('[AUTH] Checking stored user...'); const { value } = await Preferences.get({ key: STORAGE_KEYS.USER }); if (value) { const user = JSON.parse(value); console.log('[AUTH] Found stored user:', user.email || user.id); // Verify token is still valid by fetching fresh user data try { console.log('[AUTH] Fetching fresh user data from /api/me...'); const freshUser = await apiClient('/api/me'); if (freshUser) { console.log('[AUTH] Fresh user data received:', freshUser.email || freshUser.id); 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); console.log('[AUTH] Falling back to cached user data'); } return user; } console.log('[AUTH] No stored user found'); return null; } catch (e) { console.error('[AUTH] Failed to get current user:', e); console.error('[AUTH] Error details:', { message: e.message, name: e.name, stack: e.stack }); return null; } } export async function isAuthenticated() { console.log('[AUTH] Checking authentication status...'); const token = await getAuthToken(); console.log('[AUTH] Token exists:', !!token); if (!token) { console.log('[AUTH] No token found, user is not authenticated'); return false; } console.log('[AUTH] Token found, fetching current user...'); const user = await getCurrentUser(); const isAuth = !!user; console.log('[AUTH] Authentication result:', isAuth, user ? `(User: ${user.email || user.id})` : ''); return isAuth; } // 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'); } 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; } } export async function runOpencodeTask(appId, taskName, args = []) { if (!appId || appId.trim() === '') { throw new Error('appId is required'); } if (!taskName || taskName.trim() === '') { throw new Error('taskName is required'); } 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, 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 };