Files
shopify-ai-backup/android-app/capacitor-bridge.js
southseact-3d 0c954449d3 Fix Android app connectivity issues and add detailed logging
- Add CORS headers to backend server to allow mobile app requests
- Implement request timeout (10s) in capacitor-bridge.js to prevent hanging
- Add comprehensive logging throughout authentication flow
- Add detailed error reporting in initApp for better debugging
- Log all API requests with request IDs for traceability

This fixes the 'Loading Plugin Compass...' infinite loop issue caused by
missing CORS headers and unhandled network timeouts.
2026-02-17 10:20:11 +00:00

662 lines
17 KiB
JavaScript

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