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
This commit is contained in:
Developer
2026-02-16 22:02:46 +00:00
parent 3f6e649965
commit eced327702
3 changed files with 918 additions and 154 deletions

View File

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

View File

@@ -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",

View File

@@ -50,6 +50,11 @@ async function injectMetaTags() {
html = html.replace('</head>', ' <script src="capacitor.js"></script>\n</head>');
}
// Add capacitor-bridge.js before closing body tag
if (!html.includes('capacitor-bridge.js')) {
html = html.replace('</body>', ' <script type="module" src="capacitor-bridge.js"></script>\n</body>');
}
await fs.writeFile(fullPath, html, "utf8");
})
);
@@ -172,6 +177,10 @@ async function createMobileIndex() {
color: var(--white);
}
.btn-primary:active { transform: scale(0.98); }
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: var(--white);
color: var(--text);
@@ -208,6 +217,10 @@ async function createMobileIndex() {
cursor: pointer;
}
.oauth-btn:active { background: #f5f5f5; }
.oauth-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error-message {
color: var(--error);
font-size: 14px;
@@ -351,6 +364,60 @@ async function createMobileIndex() {
.quick-action-text { flex: 1; }
.quick-action-title { font-weight: 600; font-size: 16px; }
.quick-action-desc { font-size: 13px; color: var(--muted); }
/* Plugins List Screen */
.plugins-screen { background: var(--bg); }
.plugins-list {
padding: 20px;
overflow-y: auto;
}
.plugin-card {
background: var(--white);
border-radius: 16px;
padding: 16px;
margin-bottom: 12px;
border: 2px solid var(--border);
cursor: pointer;
transition: all 0.2s;
}
.plugin-card:active { transform: scale(0.98); border-color: var(--primary); }
.plugin-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.plugin-name { font-weight: 600; font-size: 16px; }
.plugin-status {
font-size: 12px;
padding: 4px 8px;
border-radius: 12px;
background: var(--bg);
}
.plugin-status.active { background: #d1fae5; color: #065f46; }
.plugin-desc { font-size: 14px; color: var(--muted); margin-bottom: 8px; }
.plugin-meta { font-size: 12px; color: var(--muted); }
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--muted);
}
.empty-state svg {
width: 64px;
height: 64px;
margin-bottom: 16px;
opacity: 0.3;
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
@@ -370,25 +437,18 @@ async function createMobileIndex() {
<div class="auth-form">
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" id="auth-email" class="form-input" placeholder="you@example.com" autocapitalize="off">
<input type="email" id="auth-email" class="form-input" placeholder="you@example.com" autocapitalize="off" autocomplete="email">
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input type="password" id="auth-password" class="form-input" placeholder="Enter your password">
<input type="password" id="auth-password" class="form-input" placeholder="Enter your password" autocomplete="current-password">
</div>
<button id="auth-submit" class="btn btn-primary">Sign In</button>
<button id="auth-submit" class="btn btn-primary">
<span id="auth-submit-text">Sign In</span>
</button>
<div class="auth-divider"><span>or</span></div>
<button id="auth-signup" class="btn btn-secondary">Create Account</button>
<div id="auth-error" class="error-message"></div>
<div class="auth-divider"><span>or continue with</span></div>
<div class="oauth-buttons">
<button id="oauth-google" class="oauth-btn">
<svg width="20" height="20" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
Continue with Google
</button>
<button id="oauth-github" class="oauth-btn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
Continue with GitHub
</button>
</div>
</div>
</div>
@@ -491,102 +551,108 @@ async function createMobileIndex() {
</div>
<div class="quick-action-text">
<div class="quick-action-title">My Plugins</div>
<div class="quick-action-desc">View your saved plugins</div>
</div>
</div>
<div class="quick-action" id="action-templates">
<div class="quick-action-icon" style="background: linear-gradient(135deg, #F59E0B, #D97706);">
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>
</div>
<div class="quick-action-text">
<div class="quick-action-title">Templates</div>
<div class="quick-action-desc">Start from a template</div>
<div class="quick-action-desc">View and edit your plugins</div>
</div>
</div>
</div>
</div>
</div>
<script>
<!-- Plugins List Screen -->
<div id="plugins-screen" class="screen plugins-screen">
<div class="main-header">
<button id="plugins-back" style="background: none; border: none; padding: 8px; margin-right: 8px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
</button>
<div class="main-title">My Plugins</div>
<button id="plugins-refresh" style="background: none; border: none; padding: 8px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--muted)" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
</button>
</div>
<div class="plugins-list" id="plugins-list">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
<p>Loading your plugins...</p>
</div>
</div>
</div>
<script type="module">
import {
loginWithEmail,
signupWithEmail,
logout,
getCurrentUser,
isAuthenticated,
getUserApps,
getApp,
createApp,
updateApp,
deleteApp,
Preferences
} from './capacitor-bridge.js';
const APP_ID = 'com.plugincompass.app';
const STORAGE_KEY = 'plugin_compass_user';
const ONBOARDING_KEY = 'plugin_compass_onboarding_done';
let currentStep = 1;
const totalSteps = 4;
let isLoading = false;
// Screen management
function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(screenId + '-screen').classList.add('active');
const screen = document.getElementById(screenId + '-screen');
if (screen) {
screen.classList.add('active');
}
}
// Auth functions
// Initialize app
async function initApp() {
const user = await getStoredUser();
console.log('[APP] Initializing...');
if (user && user.email) {
const onboardingDone = await isOnboardingComplete();
if (!onboardingDone) {
showScreen('onboarding');
try {
// Check if user is authenticated
const authenticated = await isAuthenticated();
console.log('[APP] Authenticated:', authenticated);
if (authenticated) {
const user = await getCurrentUser();
const onboardingDone = await isOnboardingComplete();
if (!onboardingDone) {
showScreen('onboarding');
} else {
showScreen('main');
updateWelcome(user);
}
} else {
showScreen('main');
updateWelcome(user);
showScreen('auth');
}
} else {
} catch (error) {
console.error('[APP] Initialization error:', error);
showScreen('auth');
}
}
async function getStoredUser() {
try {
const { Preferences } = await import('./capacitor-bridge.js');
const { value } = await Preferences.get({ key: STORAGE_KEY });
return value ? JSON.parse(value) : null;
} catch {
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? JSON.parse(stored) : null;
}
}
async function storeUser(user) {
try {
const { Preferences } = await import('./capacitor-bridge.js');
await Preferences.set({ key: STORAGE_KEY, value: JSON.stringify(user) });
} catch {
localStorage.setItem(STORAGE_KEY, JSON.stringify(user));
}
}
async function isOnboardingComplete() {
try {
const { Preferences } = await import('./capacitor-bridge.js');
const { value } = await Preferences.get({ key: ONBOARDING_KEY });
return value === 'true';
} catch {
} catch (e) {
return localStorage.getItem(ONBOARDING_KEY) === 'true';
}
}
async function completeOnboarding() {
try {
const { Preferences } = await import('./capacitor-bridge.js');
await Preferences.set({ key: ONBOARDING_KEY, value: 'true' });
} catch {
} catch (e) {
localStorage.setItem(ONBOARDING_KEY, 'true');
}
}
async function logout() {
try {
const { Preferences } = await import('./capacitor-bridge.js');
await Preferences.remove({ key: STORAGE_KEY });
} catch {
localStorage.removeItem(STORAGE_KEY);
}
showScreen('auth');
}
function updateWelcome(user) {
const welcomeEl = document.getElementById('welcome-user');
if (welcomeEl && user && user.email) {
@@ -599,107 +665,296 @@ async function createMobileIndex() {
function showOnboardingStep(step) {
currentStep = step;
// Hide all steps
for (let i = 1; i <= totalSteps; i++) {
document.getElementById('step-' + i).style.display = i === step ? 'flex' : 'none';
const stepEl = document.getElementById('step-' + i);
if (stepEl) {
stepEl.style.display = i === step ? 'flex' : 'none';
}
}
// Update progress dots
document.querySelectorAll('.progress-dot').forEach((dot, index) => {
dot.classList.remove('active', 'completed');
if (index + 1 < step) dot.classList.add('completed');
if (index + 1 === step) dot.classList.add('active');
});
// Update buttons
const backBtn = document.getElementById('onboarding-back');
const nextBtn = document.getElementById('onboarding-next');
backBtn.style.display = step === 1 ? 'none' : 'block';
nextBtn.textContent = step === totalSteps ? 'Get Started' : 'Next';
if (backBtn) backBtn.style.display = step === 1 ? 'none' : 'block';
if (nextBtn) nextBtn.textContent = step === totalSteps ? 'Get Started' : 'Next';
}
// Load and display plugins
async function loadPlugins() {
const listEl = document.getElementById('plugins-list');
if (!listEl) return;
listEl.innerHTML = '<div class="empty-state"><div class="loading-spinner"></div><p>Loading plugins...</p></div>';
try {
const result = await getUserApps();
if (!result.success) {
listEl.innerHTML = \`<div class="empty-state"><p>\${result.error || 'Failed to load plugins'}</p></div>\`;
return;
}
const apps = result.apps || [];
if (apps.length === 0) {
listEl.innerHTML = \`
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
<p>No plugins yet</p>
<p style="font-size: 14px; margin-top: 8px;">Create your first plugin to get started!</p>
</div>
\`;
return;
}
listEl.innerHTML = apps.map(app => \`
<div class="plugin-card" data-app-id="\${app.id}">
<div class="plugin-header">
<div class="plugin-name">\${app.name || 'Untitled Plugin'}</div>
<div class="plugin-status \${app.status === 'active' ? 'active' : ''}">\${app.status || 'Draft'}</div>
</div>
<div class="plugin-desc">\${app.description || 'No description'}</div>
<div class="plugin-meta">\${app.updatedAt ? new Date(app.updatedAt).toLocaleDateString() : 'Never updated'}</div>
</div>
\`).join('');
// Add click handlers
listEl.querySelectorAll('.plugin-card').forEach(card => {
card.addEventListener('click', () => {
const appId = card.dataset.appId;
// Navigate to builder with app ID
window.location.href = \`builder.html?appId=\${encodeURIComponent(appId)}\`;
});
});
} catch (error) {
console.error('[PLUGINS] Failed to load:', error);
listEl.innerHTML = '<div class="empty-state"><p>Failed to load plugins. Try again.</p></div>';
}
}
// Event listeners
document.addEventListener('DOMContentLoaded', () => {
// Auth form
document.getElementById('auth-submit').addEventListener('click', async () => {
const email = document.getElementById('auth-email').value.trim();
const password = document.getElementById('auth-password').value;
const errorEl = document.getElementById('auth-error');
if (!email) {
errorEl.textContent = 'Please enter your email';
return;
}
errorEl.textContent = '';
// Store user (in real app, would verify with server)
await storeUser({ email, createdAt: Date.now() });
showScreen('onboarding');
});
// Auth form - Sign In
const authSubmitBtn = document.getElementById('auth-submit');
const authSignupBtn = document.getElementById('auth-signup');
// OAuth buttons (would integrate with real OAuth in production)
document.getElementById('oauth-google').addEventListener('click', () => {
document.getElementById('auth-error').textContent = 'Google sign-in coming soon';
});
if (authSubmitBtn) {
authSubmitBtn.addEventListener('click', async () => {
if (isLoading) return;
const emailEl = document.getElementById('auth-email');
const passwordEl = document.getElementById('auth-password');
const errorEl = document.getElementById('auth-error');
const submitText = document.getElementById('auth-submit-text');
const email = emailEl?.value?.trim();
const password = passwordEl?.value;
if (!email) {
if (errorEl) errorEl.textContent = 'Please enter your email';
if (emailEl) emailEl.focus();
return;
}
if (!password) {
if (errorEl) errorEl.textContent = 'Please enter your password';
if (passwordEl) passwordEl.focus();
return;
}
if (errorEl) errorEl.textContent = '';
// Show loading state
isLoading = true;
authSubmitBtn.disabled = true;
if (authSignupBtn) authSignupBtn.disabled = true;
if (submitText) submitText.innerHTML = '<div class="loading-spinner" style="width: 16px; height: 16px; border-width: 2px; display: inline-block; vertical-align: middle; margin-right: 8px;"></div>Signing in...';
try {
console.log('[AUTH] Attempting login...');
const result = await loginWithEmail(email, password);
console.log('[AUTH] Login result:', result);
if (result.success) {
const onboardingDone = await isOnboardingComplete();
if (!onboardingDone) {
showScreen('onboarding');
} else {
showScreen('main');
updateWelcome(result.user);
}
} else {
if (errorEl) errorEl.textContent = result.error || 'Sign in failed. Please try again.';
}
} catch (error) {
console.error('[AUTH] Login error:', error);
if (errorEl) errorEl.textContent = error.message || 'An error occurred. Please try again.';
} finally {
isLoading = false;
authSubmitBtn.disabled = false;
if (authSignupBtn) authSignupBtn.disabled = false;
if (submitText) submitText.textContent = 'Sign In';
}
});
}
document.getElementById('oauth-github').addEventListener('click', () => {
document.getElementById('auth-error').textContent = 'GitHub sign-in coming soon';
});
// Auth form - Sign Up
if (authSignupBtn) {
authSignupBtn.addEventListener('click', async () => {
if (isLoading) return;
const emailEl = document.getElementById('auth-email');
const passwordEl = document.getElementById('auth-password');
const errorEl = document.getElementById('auth-error');
const submitText = document.getElementById('auth-submit-text');
const email = emailEl?.value?.trim();
const password = passwordEl?.value;
if (!email) {
if (errorEl) errorEl.textContent = 'Please enter your email';
if (emailEl) emailEl.focus();
return;
}
if (!password || password.length < 6) {
if (errorEl) errorEl.textContent = 'Password must be at least 6 characters';
if (passwordEl) passwordEl.focus();
return;
}
if (errorEl) errorEl.textContent = '';
// Show loading state
isLoading = true;
authSubmitBtn.disabled = true;
authSignupBtn.disabled = true;
authSignupBtn.innerHTML = '<div class="loading-spinner" style="width: 16px; height: 16px; border-width: 2px; display: inline-block; vertical-align: middle; margin-right: 8px;"></div>Creating account...';
try {
console.log('[AUTH] Attempting signup...');
const result = await signupWithEmail(email, password);
console.log('[AUTH] Signup result:', result);
if (result.success) {
showScreen('onboarding');
} else {
if (errorEl) errorEl.textContent = result.error || 'Account creation failed. Please try again.';
}
} catch (error) {
console.error('[AUTH] Signup error:', error);
if (errorEl) errorEl.textContent = error.message || 'An error occurred. Please try again.';
} finally {
isLoading = false;
authSubmitBtn.disabled = false;
authSignupBtn.disabled = false;
authSignupBtn.textContent = 'Create Account';
}
});
}
// Onboarding navigation
document.getElementById('onboarding-next').addEventListener('click', async () => {
if (currentStep < totalSteps) {
showOnboardingStep(currentStep + 1);
} else {
const onboardingNext = document.getElementById('onboarding-next');
if (onboardingNext) {
onboardingNext.addEventListener('click', async () => {
if (currentStep < totalSteps) {
showOnboardingStep(currentStep + 1);
} else {
await completeOnboarding();
const user = await getCurrentUser();
updateWelcome(user);
showScreen('main');
}
});
}
const onboardingBack = document.getElementById('onboarding-back');
if (onboardingBack) {
onboardingBack.addEventListener('click', () => {
if (currentStep > 1) {
showOnboardingStep(currentStep - 1);
}
});
}
const onboardingSkip = document.getElementById('onboarding-skip');
if (onboardingSkip) {
onboardingSkip.addEventListener('click', async () => {
await completeOnboarding();
const user = await getStoredUser();
const user = await getCurrentUser();
updateWelcome(user);
showScreen('main');
}
});
document.getElementById('onboarding-back').addEventListener('click', () => {
if (currentStep > 1) {
showOnboardingStep(currentStep - 1);
}
});
document.getElementById('onboarding-skip').addEventListener('click', async () => {
await completeOnboarding();
const user = await getStoredUser();
updateWelcome(user);
showScreen('main');
});
});
}
// Prompt cards
document.querySelectorAll('.prompt-card').forEach(card => {
card.addEventListener('click', async () => {
const prompt = card.dataset.prompt;
await completeOnboarding();
const user = await getStoredUser();
const user = await getCurrentUser();
updateWelcome(user);
showScreen('main');
// In production, would navigate to builder with this prompt
// Navigate to builder with prompt
window.location.href = \`builder.html?prompt=\${encodeURIComponent(prompt)}\`;
});
});
// Quick actions
document.getElementById('action-new-plugin').addEventListener('click', () => {
window.location.href = 'builder.html';
});
const actionNewPlugin = document.getElementById('action-new-plugin');
if (actionNewPlugin) {
actionNewPlugin.addEventListener('click', () => {
window.location.href = 'builder.html';
});
}
document.getElementById('action-my-plugins').addEventListener('click', () => {
window.location.href = 'apps.html';
});
const actionMyPlugins = document.getElementById('action-my-plugins');
if (actionMyPlugins) {
actionMyPlugins.addEventListener('click', () => {
showScreen('plugins');
loadPlugins();
});
}
document.getElementById('action-templates').addEventListener('click', () => {
window.location.href = 'templates.html';
});
// Plugins screen
const pluginsBack = document.getElementById('plugins-back');
if (pluginsBack) {
pluginsBack.addEventListener('click', () => {
showScreen('main');
});
}
const pluginsRefresh = document.getElementById('plugins-refresh');
if (pluginsRefresh) {
pluginsRefresh.addEventListener('click', () => {
loadPlugins();
});
}
// Logout
document.getElementById('logout-btn').addEventListener('click', logout);
const logoutBtn = document.getElementById('logout-btn');
if (logoutBtn) {
logoutBtn.addEventListener('click', async () => {
try {
await logout();
showScreen('auth');
} catch (error) {
console.error('[AUTH] Logout error:', error);
}
});
}
// Initialize
setTimeout(initApp, 500);