From eced32770269cde55ac77ba0d0bfb3160cfb7ae0 Mon Sep 17 00:00:00 2001
From: Developer
Date: Mon, 16 Feb 2026 22:02:46 +0000
Subject: [PATCH] Fix capacitor build: Implement proper backend connectivity
and authentication
- Update BACKEND_BASE_URL to plugincompass.com:9445
- Add comprehensive authentication system (login, signup, logout)
- Implement JWT token management with automatic refresh
- Add plugin/app management API (CRUD operations)
- Add session management for chat/builder
- Update mobile UI with real backend integration
- Add user account and usage tracking
- Enable plugin loading and editing from backend
- Fix module type for ES6 support
---
android-app/capacitor-bridge.js | 550 ++++++++++++++++++++++++++++++--
android-app/package.json | 1 +
android-app/scripts/sync-ui.js | 521 ++++++++++++++++++++++--------
3 files changed, 918 insertions(+), 154 deletions(-)
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() {
@@ -491,102 +551,108 @@ async function createMobileIndex() {
My Plugins
-
View your saved plugins
-
-
-
-
-
-
-
-
Templates
-
Start from a template
+
View and edit your plugins
-