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:
@@ -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;
|
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) {
|
export async function syncApp(appId) {
|
||||||
if (!appId || appId.trim() === '') {
|
if (!appId || appId.trim() === '') {
|
||||||
throw new Error('appId is required');
|
throw new Error('appId is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { value } = await Preferences.get({ key: `app_${appId}` });
|
try {
|
||||||
if (!value) {
|
// Get app from backend instead of local storage
|
||||||
throw new Error(`App not found: ${appId}`);
|
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 = []) {
|
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.');
|
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 = {
|
window.nativeBridge = {
|
||||||
Preferences,
|
Preferences,
|
||||||
syncApp,
|
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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "plugin-compass-android",
|
"name": "plugin-compass-android",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare-ui": "node ./scripts/sync-ui.js",
|
"prepare-ui": "node ./scripts/sync-ui.js",
|
||||||
"build": "npm run prepare-ui && npx cap sync android",
|
"build": "npm run prepare-ui && npx cap sync android",
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ async function injectMetaTags() {
|
|||||||
html = html.replace('</head>', ' <script src="capacitor.js"></script>\n</head>');
|
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");
|
await fs.writeFile(fullPath, html, "utf8");
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -172,6 +177,10 @@ async function createMobileIndex() {
|
|||||||
color: var(--white);
|
color: var(--white);
|
||||||
}
|
}
|
||||||
.btn-primary:active { transform: scale(0.98); }
|
.btn-primary:active { transform: scale(0.98); }
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -208,6 +217,10 @@ async function createMobileIndex() {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.oauth-btn:active { background: #f5f5f5; }
|
.oauth-btn:active { background: #f5f5f5; }
|
||||||
|
.oauth-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
.error-message {
|
.error-message {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -351,6 +364,60 @@ async function createMobileIndex() {
|
|||||||
.quick-action-text { flex: 1; }
|
.quick-action-text { flex: 1; }
|
||||||
.quick-action-title { font-weight: 600; font-size: 16px; }
|
.quick-action-title { font-weight: 600; font-size: 16px; }
|
||||||
.quick-action-desc { font-size: 13px; color: var(--muted); }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -370,25 +437,18 @@ async function createMobileIndex() {
|
|||||||
<div class="auth-form">
|
<div class="auth-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Email</label>
|
<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>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Password</label>
|
<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>
|
</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 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -491,102 +551,108 @@ async function createMobileIndex() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="quick-action-text">
|
<div class="quick-action-text">
|
||||||
<div class="quick-action-title">My Plugins</div>
|
<div class="quick-action-title">My Plugins</div>
|
||||||
<div class="quick-action-desc">View your saved plugins</div>
|
<div class="quick-action-desc">View and edit your 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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 APP_ID = 'com.plugincompass.app';
|
||||||
const STORAGE_KEY = 'plugin_compass_user';
|
|
||||||
const ONBOARDING_KEY = 'plugin_compass_onboarding_done';
|
const ONBOARDING_KEY = 'plugin_compass_onboarding_done';
|
||||||
|
|
||||||
let currentStep = 1;
|
let currentStep = 1;
|
||||||
const totalSteps = 4;
|
const totalSteps = 4;
|
||||||
|
let isLoading = false;
|
||||||
|
|
||||||
// Screen management
|
// Screen management
|
||||||
function showScreen(screenId) {
|
function showScreen(screenId) {
|
||||||
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
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() {
|
async function initApp() {
|
||||||
const user = await getStoredUser();
|
console.log('[APP] Initializing...');
|
||||||
|
|
||||||
if (user && user.email) {
|
try {
|
||||||
const onboardingDone = await isOnboardingComplete();
|
// Check if user is authenticated
|
||||||
if (!onboardingDone) {
|
const authenticated = await isAuthenticated();
|
||||||
showScreen('onboarding');
|
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 {
|
} else {
|
||||||
showScreen('main');
|
showScreen('auth');
|
||||||
updateWelcome(user);
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
|
console.error('[APP] Initialization error:', error);
|
||||||
showScreen('auth');
|
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() {
|
async function isOnboardingComplete() {
|
||||||
try {
|
try {
|
||||||
const { Preferences } = await import('./capacitor-bridge.js');
|
|
||||||
const { value } = await Preferences.get({ key: ONBOARDING_KEY });
|
const { value } = await Preferences.get({ key: ONBOARDING_KEY });
|
||||||
return value === 'true';
|
return value === 'true';
|
||||||
} catch {
|
} catch (e) {
|
||||||
return localStorage.getItem(ONBOARDING_KEY) === 'true';
|
return localStorage.getItem(ONBOARDING_KEY) === 'true';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completeOnboarding() {
|
async function completeOnboarding() {
|
||||||
try {
|
try {
|
||||||
const { Preferences } = await import('./capacitor-bridge.js');
|
|
||||||
await Preferences.set({ key: ONBOARDING_KEY, value: 'true' });
|
await Preferences.set({ key: ONBOARDING_KEY, value: 'true' });
|
||||||
} catch {
|
} catch (e) {
|
||||||
localStorage.setItem(ONBOARDING_KEY, 'true');
|
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) {
|
function updateWelcome(user) {
|
||||||
const welcomeEl = document.getElementById('welcome-user');
|
const welcomeEl = document.getElementById('welcome-user');
|
||||||
if (welcomeEl && user && user.email) {
|
if (welcomeEl && user && user.email) {
|
||||||
@@ -599,107 +665,296 @@ async function createMobileIndex() {
|
|||||||
function showOnboardingStep(step) {
|
function showOnboardingStep(step) {
|
||||||
currentStep = step;
|
currentStep = step;
|
||||||
|
|
||||||
// Hide all steps
|
|
||||||
for (let i = 1; i <= totalSteps; i++) {
|
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) => {
|
document.querySelectorAll('.progress-dot').forEach((dot, index) => {
|
||||||
dot.classList.remove('active', 'completed');
|
dot.classList.remove('active', 'completed');
|
||||||
if (index + 1 < step) dot.classList.add('completed');
|
if (index + 1 < step) dot.classList.add('completed');
|
||||||
if (index + 1 === step) dot.classList.add('active');
|
if (index + 1 === step) dot.classList.add('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update buttons
|
|
||||||
const backBtn = document.getElementById('onboarding-back');
|
const backBtn = document.getElementById('onboarding-back');
|
||||||
const nextBtn = document.getElementById('onboarding-next');
|
const nextBtn = document.getElementById('onboarding-next');
|
||||||
|
|
||||||
backBtn.style.display = step === 1 ? 'none' : 'block';
|
if (backBtn) backBtn.style.display = step === 1 ? 'none' : 'block';
|
||||||
nextBtn.textContent = step === totalSteps ? 'Get Started' : 'Next';
|
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
|
// Event listeners
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Auth form
|
// Auth form - Sign In
|
||||||
document.getElementById('auth-submit').addEventListener('click', async () => {
|
const authSubmitBtn = document.getElementById('auth-submit');
|
||||||
const email = document.getElementById('auth-email').value.trim();
|
const authSignupBtn = document.getElementById('auth-signup');
|
||||||
const password = document.getElementById('auth-password').value;
|
|
||||||
const errorEl = document.getElementById('auth-error');
|
|
||||||
|
|
||||||
if (!email) {
|
if (authSubmitBtn) {
|
||||||
errorEl.textContent = 'Please enter your email';
|
authSubmitBtn.addEventListener('click', async () => {
|
||||||
return;
|
if (isLoading) return;
|
||||||
}
|
|
||||||
|
|
||||||
errorEl.textContent = '';
|
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');
|
||||||
|
|
||||||
// Store user (in real app, would verify with server)
|
const email = emailEl?.value?.trim();
|
||||||
await storeUser({ email, createdAt: Date.now() });
|
const password = passwordEl?.value;
|
||||||
showScreen('onboarding');
|
|
||||||
});
|
|
||||||
|
|
||||||
// OAuth buttons (would integrate with real OAuth in production)
|
if (!email) {
|
||||||
document.getElementById('oauth-google').addEventListener('click', () => {
|
if (errorEl) errorEl.textContent = 'Please enter your email';
|
||||||
document.getElementById('auth-error').textContent = 'Google sign-in coming soon';
|
if (emailEl) emailEl.focus();
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('oauth-github').addEventListener('click', () => {
|
if (!password) {
|
||||||
document.getElementById('auth-error').textContent = 'GitHub sign-in coming soon';
|
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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// Onboarding navigation
|
||||||
document.getElementById('onboarding-next').addEventListener('click', async () => {
|
const onboardingNext = document.getElementById('onboarding-next');
|
||||||
if (currentStep < totalSteps) {
|
if (onboardingNext) {
|
||||||
showOnboardingStep(currentStep + 1);
|
onboardingNext.addEventListener('click', async () => {
|
||||||
} else {
|
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();
|
await completeOnboarding();
|
||||||
const user = await getStoredUser();
|
const user = await getCurrentUser();
|
||||||
updateWelcome(user);
|
updateWelcome(user);
|
||||||
showScreen('main');
|
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
|
// Prompt cards
|
||||||
document.querySelectorAll('.prompt-card').forEach(card => {
|
document.querySelectorAll('.prompt-card').forEach(card => {
|
||||||
card.addEventListener('click', async () => {
|
card.addEventListener('click', async () => {
|
||||||
const prompt = card.dataset.prompt;
|
const prompt = card.dataset.prompt;
|
||||||
await completeOnboarding();
|
await completeOnboarding();
|
||||||
const user = await getStoredUser();
|
const user = await getCurrentUser();
|
||||||
updateWelcome(user);
|
updateWelcome(user);
|
||||||
showScreen('main');
|
// Navigate to builder with prompt
|
||||||
// In production, would navigate to builder with this prompt
|
window.location.href = \`builder.html?prompt=\${encodeURIComponent(prompt)}\`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quick actions
|
// Quick actions
|
||||||
document.getElementById('action-new-plugin').addEventListener('click', () => {
|
const actionNewPlugin = document.getElementById('action-new-plugin');
|
||||||
window.location.href = 'builder.html';
|
if (actionNewPlugin) {
|
||||||
});
|
actionNewPlugin.addEventListener('click', () => {
|
||||||
|
window.location.href = 'builder.html';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('action-my-plugins').addEventListener('click', () => {
|
const actionMyPlugins = document.getElementById('action-my-plugins');
|
||||||
window.location.href = 'apps.html';
|
if (actionMyPlugins) {
|
||||||
});
|
actionMyPlugins.addEventListener('click', () => {
|
||||||
|
showScreen('plugins');
|
||||||
|
loadPlugins();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('action-templates').addEventListener('click', () => {
|
// Plugins screen
|
||||||
window.location.href = 'templates.html';
|
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
|
// 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
|
// Initialize
|
||||||
setTimeout(initApp, 500);
|
setTimeout(initApp, 500);
|
||||||
|
|||||||
Reference in New Issue
Block a user