Rename to Plugin Compass, add mobile onboarding/signin, implement self-update for desktop, and fix workflow paths

This commit is contained in:
southseact-3d
2026-02-16 12:05:30 +00:00
parent 63698e1d19
commit d6e2af3a29
11 changed files with 945 additions and 138 deletions

View File

@@ -1,18 +1,19 @@
# Android App (Capacitor)
# Plugin Compass Android (Capacitor)
This folder contains the Android app build for the project. The app reuses the existing web UI (apps and builder screens) and wraps it with Capacitor for native Android deployment.
This folder contains the Android app build for Plugin Compass. The app provides a native mobile experience with sign-in, onboarding, and plugin building features.
## Goals
- Reuse the existing web UI with minimal divergence.
- Provide a native Android APK for distribution.
- Sync apps between device and backend.
- Build via GitHub Actions; do not build locally.
## Features
- Native sign-in screen with email and OAuth options.
- Full onboarding flow with step-by-step guidance.
- Quick start prompts to help users get started.
- Reuses the existing web UI for builder and apps pages.
- Build via GitHub Actions.
## How it works
- **UI reuse:** `npm run prepare-ui` copies `../chat/public` into `www` and injects a bridge script so existing pages can talk to native APIs.
- **Native bridge:** Capacitor provides native access to preferences and HTTP requests.
- **Local storage:** Apps are saved using Capacitor Preferences plugin, isolated per user.
- **Syncing:** `syncApp` posts the locally saved app JSON to the backend.
- **Sign-in**: Custom mobile-first sign-in screen with email/password and OAuth buttons.
- **Onboarding**: 4-step onboarding flow introducing key features and quick start prompts.
- **UI reuse**: After onboarding, users access the builder and apps from the web UI.
- **Local storage**: Apps and preferences are saved using Capacitor Preferences.
## Setup
1. Install prerequisites (Node 18+, Android SDK).
@@ -29,6 +30,14 @@ This folder contains the Android app build for the project. The app reuses the e
- GitHub Actions workflow: `.github/workflows/build-android-app.yml`.
- The action runs on `ubuntu-latest`, sets up Java/Android SDK, and builds the APK.
## Custom Mobile Index
The build script creates a custom `index.html` for mobile with:
- Loading screen with branded animation
- Sign-in screen (email/password + OAuth)
- Onboarding flow (4 steps with quick start prompts)
- Main dashboard with quick actions
## Security notes
- API keys are stored in Capacitor Preferences (encrypted on Android).
- User credentials are stored securely using Capacitor Preferences.
- API keys are never exposed to the web layer.
- OpenCode execution is not supported on mobile (desktop app only).

View File

@@ -1,8 +1,62 @@
import { Preferences } from '@capacitor/preferences';
const BACKEND_BASE_URL = window.BACKEND_BASE_URL || 'https://api.example.com';
export { Preferences };
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();
}
};
export async function syncApp(appId) {
if (!appId || appId.trim() === '') {
@@ -41,3 +95,9 @@ 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.');
}
window.nativeBridge = {
Preferences,
syncApp,
runOpencodeTask
};

View File

@@ -1,6 +1,6 @@
{
"appId": "com.shopifyai.app",
"appName": "ShopifyAI",
"appId": "com.plugincompass.app",
"appName": "Plugin Compass",
"webDir": "www",
"server": {
"androidScheme": "https"
@@ -10,7 +10,7 @@
"enabled": true
},
"Preferences": {
"group": "ShopifyAI"
"group": "PluginCompass"
}
},
"android": {

View File

@@ -1,11 +1,11 @@
{
"name": "shopify-ai-android-app",
"name": "plugin-compass-android",
"version": "0.1.0",
"private": true,
"scripts": {
"prepare-ui": "node ./scripts/sync-ui.js",
"build": "npm run prepare-ui && npx cap sync android",
"cap:init": "npx cap init 'ShopifyAI' com.shopifyai.app --web-dir www",
"cap:init": "npx cap init 'Plugin Compass' com.plugincompass.app --web-dir www",
"cap:add": "npx cap add android",
"cap:sync": "npx cap sync android",
"cap:open": "npx cap open android",

View File

@@ -34,58 +34,687 @@ async function findHtmlFiles(dir) {
return files.flat();
}
async function injectBridge() {
const bridgeContent = `
<script>
window.nativeBridge = {
async saveApiKey(token) {
const { Preferences } = await import('./capacitor-bridge.js');
return Preferences.set({ key: 'opencode_api_key', value: token });
},
async persistApp(app) {
const { Preferences } = await import('./capacitor-bridge.js');
return Preferences.set({ key: 'app_' + app.id, value: JSON.stringify(app) });
},
async listApps() {
const { Preferences } = await import('./capacitor-bridge.js');
const keys = await Preferences.keys();
const apps = [];
for (const key of keys.keys) {
if (key.startsWith('app_')) {
const { value } = await Preferences.get({ key });
if (value) apps.push(JSON.parse(value));
}
}
return apps;
},
async syncApp(appId) {
const { syncApp } = await import('./capacitor-bridge.js');
return syncApp(appId);
},
async runOpencodeTask(appId, taskName, args) {
const { runOpencodeTask } = await import('./capacitor-bridge.js');
return runOpencodeTask(appId, taskName, args);
}
};
</script>
`;
async function injectMetaTags() {
const files = await findHtmlFiles(dest);
await Promise.all(
files.map(async (fullPath) => {
const html = await fs.readFile(fullPath, "utf8");
if (html.includes("capacitor-bridge.js")) return;
let html = await fs.readFile(fullPath, "utf8");
const injection = bridgeContent + '\n </head>';
const updated = html.replace(/<\/head>/i, injection);
await fs.writeFile(fullPath, updated, "utf8");
// Add viewport meta if missing
if (!html.includes('viewport')) {
html = html.replace('<head>', '<head>\n <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">');
}
// Add Capacitor script
if (!html.includes('capacitor.js')) {
html = html.replace('</head>', ' <script src="capacitor.js"></script>\n</head>');
}
await fs.writeFile(fullPath, html, "utf8");
})
);
}
async function createMobileIndex() {
const mobileIndex = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Plugin Compass</title>
<link rel="icon" type="image/png" href="assets/Plugin.png">
<script src="capacitor.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #008060;
--primary-dark: #004c3f;
--bg: #fdf6ed;
--text: #1a1a1a;
--muted: #6b7280;
--border: #e5e7eb;
--white: #ffffff;
--error: #dc2626;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
}
.screen { display: none; flex-direction: column; min-height: 100vh; }
.screen.active { display: flex; }
/* Loading Screen */
.loading-screen {
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
}
.loading-logo {
width: 80px;
height: 80px;
border-radius: 20px;
margin-bottom: 24px;
animation: pulse 1.5s ease-in-out infinite;
}
.loading-text {
color: var(--white);
font-size: 18px;
font-weight: 600;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.8; }
}
/* Auth Screen */
.auth-screen { background: var(--bg); }
.auth-header {
padding: 48px 24px 32px;
text-align: center;
}
.auth-logo {
width: 64px;
height: 64px;
border-radius: 16px;
margin-bottom: 16px;
}
.auth-title {
font-size: 28px;
font-weight: 700;
color: var(--text);
margin-bottom: 8px;
}
.auth-subtitle {
font-size: 16px;
color: var(--muted);
}
.auth-form {
padding: 0 24px;
flex: 1;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 600;
color: var(--text);
margin-bottom: 6px;
}
.form-input {
width: 100%;
padding: 14px 16px;
font-size: 16px;
border: 2px solid var(--border);
border-radius: 12px;
background: var(--white);
outline: none;
transition: border-color 0.2s;
}
.form-input:focus { border-color: var(--primary); }
.btn {
width: 100%;
padding: 16px;
font-size: 16px;
font-weight: 700;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: var(--white);
}
.btn-primary:active { transform: scale(0.98); }
.btn-secondary {
background: var(--white);
color: var(--text);
border: 2px solid var(--border);
margin-top: 12px;
}
.auth-divider {
display: flex;
align-items: center;
margin: 24px 0;
color: var(--muted);
font-size: 14px;
}
.auth-divider::before, .auth-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.auth-divider span { padding: 0 16px; }
.oauth-buttons { display: flex; flex-direction: column; gap: 12px; }
.oauth-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 14px;
background: var(--white);
border: 2px solid var(--border);
border-radius: 12px;
font-size: 16px;
font-weight: 600;
color: var(--text);
cursor: pointer;
}
.oauth-btn:active { background: #f5f5f5; }
.error-message {
color: var(--error);
font-size: 14px;
text-align: center;
margin-top: 12px;
min-height: 20px;
}
/* Onboarding Screen */
.onboarding-screen { background: var(--white); }
.onboarding-header {
padding: 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.onboarding-skip {
color: var(--muted);
font-size: 16px;
background: none;
border: none;
cursor: pointer;
}
.onboarding-progress {
display: flex;
gap: 8px;
justify-content: center;
padding: 0 24px 24px;
}
.progress-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--border);
transition: all 0.3s;
}
.progress-dot.active { background: var(--primary); transform: scale(1.25); }
.progress-dot.completed { background: var(--primary); }
.onboarding-content {
flex: 1;
padding: 0 32px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.onboarding-icon {
width: 100px;
height: 100px;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 32px;
}
.onboarding-icon svg { width: 48px; height: 48px; stroke: white; }
.onboarding-title {
font-size: 28px;
font-weight: 700;
color: var(--text);
margin-bottom: 16px;
}
.onboarding-text {
font-size: 17px;
color: var(--muted);
line-height: 1.6;
max-width: 320px;
}
.onboarding-prompts {
width: 100%;
margin-top: 32px;
}
.prompt-card {
background: var(--bg);
border: 2px solid var(--border);
border-radius: 16px;
padding: 16px;
margin-bottom: 12px;
text-align: left;
cursor: pointer;
transition: all 0.2s;
}
.prompt-card:active { transform: scale(0.98); border-color: var(--primary); }
.prompt-title { font-weight: 600; color: var(--text); margin-bottom: 4px; }
.prompt-desc { font-size: 14px; color: var(--muted); }
.onboarding-footer {
padding: 24px;
display: flex;
gap: 12px;
}
.onboarding-footer .btn { flex: 1; }
/* Main Screen */
.main-screen { background: var(--bg); }
.main-header {
background: var(--white);
padding: 16px 20px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid var(--border);
}
.main-logo { width: 36px; height: 36px; border-radius: 10px; }
.main-title { font-size: 18px; font-weight: 700; flex: 1; }
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.welcome-card {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
border-radius: 20px;
padding: 24px;
color: var(--white);
margin-bottom: 20px;
}
.welcome-title { font-size: 22px; font-weight: 700; margin-bottom: 8px; }
.welcome-text { font-size: 15px; opacity: 0.9; }
.quick-actions { display: flex; flex-direction: column; gap: 12px; }
.quick-action {
background: var(--white);
border-radius: 16px;
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
border: 2px solid var(--border);
cursor: pointer;
transition: all 0.2s;
}
.quick-action:active { transform: scale(0.98); border-color: var(--primary); }
.quick-action-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.quick-action-icon svg { width: 22px; height: 22px; }
.quick-action-text { flex: 1; }
.quick-action-title { font-weight: 600; font-size: 16px; }
.quick-action-desc { font-size: 13px; color: var(--muted); }
</style>
</head>
<body>
<!-- Loading Screen -->
<div id="loading-screen" class="screen active loading-screen">
<img src="assets/Plugin.png" class="loading-logo" alt="Plugin Compass">
<div class="loading-text">Loading Plugin Compass...</div>
</div>
<!-- Auth Screen -->
<div id="auth-screen" class="screen auth-screen">
<div class="auth-header">
<img src="assets/Plugin.png" class="auth-logo" alt="Plugin Compass">
<h1 class="auth-title">Plugin Compass</h1>
<p class="auth-subtitle">Build WordPress plugins with AI</p>
</div>
<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">
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input type="password" id="auth-password" class="form-input" placeholder="Enter your password">
</div>
<button id="auth-submit" class="btn btn-primary">Sign In</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>
<!-- Onboarding Screen -->
<div id="onboarding-screen" class="screen onboarding-screen">
<div class="onboarding-header">
<div></div>
<button id="onboarding-skip" class="onboarding-skip">Skip</button>
</div>
<div class="onboarding-progress">
<div class="progress-dot active" data-step="1"></div>
<div class="progress-dot" data-step="2"></div>
<div class="progress-dot" data-step="3"></div>
<div class="progress-dot" data-step="4"></div>
</div>
<!-- Step 1: Welcome -->
<div id="step-1" class="onboarding-content" style="display: flex;">
<div class="onboarding-icon" style="background: linear-gradient(135deg, #008060, #004c3f);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
</div>
<h2 class="onboarding-title">Welcome to Plugin Compass</h2>
<p class="onboarding-text">Build custom WordPress plugins with AI. No coding required - just describe what you need.</p>
</div>
<!-- Step 2: Features -->
<div id="step-2" class="onboarding-content" style="display: none;">
<div class="onboarding-icon" style="background: linear-gradient(135deg, #5A31F4, #8B5CF6);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<h2 class="onboarding-title">Chat to Build</h2>
<p class="onboarding-text">Describe your plugin in plain English. Our AI understands WordPress and creates working code.</p>
</div>
<!-- Step 3: Prompts -->
<div id="step-3" class="onboarding-content" style="display: none;">
<div class="onboarding-icon" style="background: linear-gradient(135deg, #F59E0B, #D97706);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</div>
<h2 class="onboarding-title">Quick Start Ideas</h2>
<p class="onboarding-text" style="margin-bottom: 20px;">Tap a suggestion to get started:</p>
<div class="onboarding-prompts">
<div class="prompt-card" data-prompt="Create a contact form plugin with spam protection and email notifications">
<div class="prompt-title">Contact Form Plugin</div>
<div class="prompt-desc">Spam protection & email notifications</div>
</div>
<div class="prompt-card" data-prompt="Build a testimonial slider plugin with admin management and shortcodes">
<div class="prompt-title">Testimonial Slider</div>
<div class="prompt-desc">Admin management & shortcodes</div>
</div>
<div class="prompt-card" data-prompt="Create an SEO meta tags plugin with social media preview support">
<div class="prompt-title">SEO Meta Tags</div>
<div class="prompt-desc">Social media preview support</div>
</div>
</div>
</div>
<!-- Step 4: Ready -->
<div id="step-4" class="onboarding-content" style="display: none;">
<div class="onboarding-icon" style="background: linear-gradient(135deg, #10B981, #059669);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
</div>
<h2 class="onboarding-title">You're All Set!</h2>
<p class="onboarding-text">Start building your first plugin. You can always ask questions or get help along the way.</p>
</div>
<div class="onboarding-footer">
<button id="onboarding-back" class="btn btn-secondary" style="display: none;">Back</button>
<button id="onboarding-next" class="btn btn-primary">Next</button>
</div>
</div>
<!-- Main Screen -->
<div id="main-screen" class="screen main-screen">
<div class="main-header">
<img src="assets/Plugin.png" class="main-logo" alt="Plugin Compass">
<div class="main-title">Plugin Compass</div>
<button id="logout-btn" 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="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
</button>
</div>
<div class="main-content">
<div class="welcome-card">
<div class="welcome-title">Ready to Build!</div>
<div class="welcome-text" id="welcome-user">What would you like to create today?</div>
</div>
<div class="quick-actions">
<div class="quick-action" id="action-new-plugin">
<div class="quick-action-icon" style="background: linear-gradient(135deg, #008060, #004c3f);">
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</div>
<div class="quick-action-text">
<div class="quick-action-title">New Plugin</div>
<div class="quick-action-desc">Start building from scratch</div>
</div>
</div>
<div class="quick-action" id="action-my-plugins">
<div class="quick-action-icon" style="background: linear-gradient(135deg, #5A31F4, #8B5CF6);">
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
</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>
</div>
</div>
</div>
</div>
<script>
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;
// Screen management
function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(screenId + '-screen').classList.add('active');
}
// Auth functions
async function initApp() {
const user = await getStoredUser();
if (user && user.email) {
const onboardingDone = await isOnboardingComplete();
if (!onboardingDone) {
showScreen('onboarding');
} else {
showScreen('main');
updateWelcome(user);
}
} else {
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 {
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 {
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) {
const name = user.email.split('@')[0];
welcomeEl.textContent = \`Hi \${name}! What would you like to build today?\`;
}
}
// Onboarding navigation
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';
}
// 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';
}
// 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');
});
// 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';
});
document.getElementById('oauth-github').addEventListener('click', () => {
document.getElementById('auth-error').textContent = 'GitHub sign-in coming soon';
});
// Onboarding navigation
document.getElementById('onboarding-next').addEventListener('click', async () => {
if (currentStep < totalSteps) {
showOnboardingStep(currentStep + 1);
} else {
await completeOnboarding();
const user = await getStoredUser();
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();
updateWelcome(user);
showScreen('main');
// In production, would navigate to builder with this prompt
});
});
// Quick actions
document.getElementById('action-new-plugin').addEventListener('click', () => {
window.location.href = 'builder.html';
});
document.getElementById('action-my-plugins').addEventListener('click', () => {
window.location.href = 'apps.html';
});
document.getElementById('action-templates').addEventListener('click', () => {
window.location.href = 'templates.html';
});
// Logout
document.getElementById('logout-btn').addEventListener('click', logout);
// Initialize
setTimeout(initApp, 500);
});
</script>
</body>
</html>`;
await fs.writeFile(path.join(dest, 'index.html'), mobileIndex, 'utf8');
}
async function main() {
await copyUi();
await injectBridge();
await injectMetaTags();
await createMobileIndex();
console.log(`UI prepared in ${dest}`);
}