mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 07:12:54 +01:00
892 lines
48 KiB
Markdown
892 lines
48 KiB
Markdown
# Capacitor Mobile Target
|
||
|
||
**Phase ID:** PHASE-B
|
||
**Priority:** 🔴 High (Primary Target)
|
||
**Estimated Duration:** 4-5 weeks
|
||
**Status:** Planning
|
||
**Dependencies:** Phase A (BYOB Backend), Phase E (Target System Core)
|
||
**Last Updated:** 2025-12-28
|
||
|
||
## Executive Summary
|
||
|
||
Enable Noodl users to build native iOS and Android applications using Capacitor as the bridge between web technologies and native device capabilities. This is the highest-priority target due to massive user demand for mobile app development without native code knowledge.
|
||
|
||
## Value Proposition
|
||
|
||
| Current State | With Capacitor Target |
|
||
|--------------|----------------------|
|
||
| Export static web, manually wrap in Capacitor | One-click Capacitor project export |
|
||
| No mobile preview | Hot-reload preview on device/simulator |
|
||
| No native API access | Camera, GPS, Push, Haptics nodes |
|
||
| Manual bridge setup | Automatic Capacitor bridge injection |
|
||
| 30-60s iteration cycles | 1-2s hot-reload cycles |
|
||
|
||
## Technical Architecture
|
||
|
||
### How Capacitor Works
|
||
|
||
Capacitor is a cross-platform native runtime that allows web apps to run natively on iOS and Android with access to native device features through JavaScript bridges.
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ CAPACITOR ARCHITECTURE │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ YOUR NOODL APP │ │
|
||
│ │ (HTML, CSS, JavaScript - same as web export) │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ CAPACITOR BRIDGE │ │
|
||
│ │ window.Capacitor.Plugins.Camera.getPhoto() │ │
|
||
│ │ window.Capacitor.Plugins.Geolocation.getCurrentPosition() │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌───────────────┼───────────────┐ │
|
||
│ ▼ ▼ ▼ │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ iOS Native │ │ Android Native │ │ Web Fallback │ │
|
||
│ │ Swift/ObjC │ │ Kotlin/Java │ │ (PWA APIs) │ │
|
||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Integration with Noodl
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ NOODL EDITOR │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ VISUAL GRAPH │ │
|
||
│ │ [Button] ──onClick──▶ [Camera Capture] ──photo──▶ [Image] │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ CAPACITOR NODE LIBRARY │ │
|
||
│ │ Camera, Geolocation, Push, Haptics, StatusBar, etc. │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌───────────────────┼───────────────────┐ │
|
||
│ ▼ ▼ ▼ │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||
│ │ PREVIEW │ │ EXPORT │ │ PREVIEW │ │
|
||
│ │ (Web) │ │ (Capacitor) │ │ (Device) │ │
|
||
│ │ │ │ │ │ │ │
|
||
│ │ Mock APIs │ │ Real build │ │ Hot-reload │ │
|
||
│ │ in browser │ │ to Xcode/ │ │ on physical │ │
|
||
│ │ │ │ Android │ │ device │ │
|
||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## Preview Modes
|
||
|
||
### 1. Web Preview (Default)
|
||
|
||
Standard browser preview with mocked Capacitor APIs:
|
||
|
||
```typescript
|
||
// When in web preview mode, inject mock Capacitor
|
||
if (!window.Capacitor) {
|
||
window.Capacitor = {
|
||
isNativePlatform: () => false,
|
||
Plugins: {
|
||
Camera: {
|
||
async getPhoto(options: CameraOptions): Promise<Photo> {
|
||
// Use browser MediaDevices API
|
||
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||
// ... capture and return photo
|
||
}
|
||
},
|
||
Geolocation: {
|
||
async getCurrentPosition(): Promise<Position> {
|
||
// Use browser Geolocation API
|
||
return new Promise((resolve, reject) => {
|
||
navigator.geolocation.getCurrentPosition(
|
||
(pos) => resolve({
|
||
coords: {
|
||
latitude: pos.coords.latitude,
|
||
longitude: pos.coords.longitude,
|
||
accuracy: pos.coords.accuracy,
|
||
}
|
||
}),
|
||
reject
|
||
);
|
||
});
|
||
}
|
||
},
|
||
// ... other mocked plugins
|
||
}
|
||
};
|
||
}
|
||
```
|
||
|
||
**Limitations:**
|
||
- Push notifications won't work (no registration token)
|
||
- Some device-specific features unavailable
|
||
- Camera quality may differ from native
|
||
|
||
### 2. Capacitor Hot-Reload Preview
|
||
|
||
Connect physical device or simulator to Noodl's dev server:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ Preview Mode: [Web ▾] │
|
||
│ ├─ Web (Browser) │
|
||
│ ├─ iOS Simulator ◀ │
|
||
│ ├─ Android Emulator │
|
||
│ └─ Physical Device │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ⚠️ Capacitor Preview Setup Required │
|
||
│ │
|
||
│ To preview on iOS Simulator: │
|
||
│ │
|
||
│ 1. Install Xcode from the Mac App Store │
|
||
│ 2. Open Terminal and run: │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ npx cap run ios --livereload-url=http://localhost:8574 │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ 3. Select a simulator when prompted │
|
||
│ │
|
||
│ [Generate Capacitor Project] [Copy Command] [Open Documentation] │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**How Hot-Reload Works:**
|
||
|
||
```
|
||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||
│ Noodl Editor │◄───────▶│ Dev Server │◄───────▶│ iOS/Android │
|
||
│ │ WebSocket│ localhost:8574│ HTTP │ App in │
|
||
│ Make changes │─────────▶│ Serves app │─────────▶ WebView │
|
||
│ │ │ + Capacitor │ │ │
|
||
└───────────────┘ │ bridge │ │ Native APIs │
|
||
└───────────────┘ │ available │
|
||
└───────────────┘
|
||
```
|
||
|
||
1. Noodl's dev server already serves the preview app at `localhost:8574`
|
||
2. Capacitor app's WebView loads from this URL instead of bundled assets
|
||
3. When you make changes in Noodl, the WebView automatically refreshes
|
||
4. Native Capacitor plugins are available because you're running in a native app
|
||
|
||
### 3. Device Preview (Physical Device)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ Preview on Device [×] │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ CONNECTED DEVICES │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ 📱 Richard's iPhone 15 Pro ✓ Connected │ │
|
||
│ │ iOS 17.2 • USB Connected │ │
|
||
│ │ [Open App] │ │
|
||
│ ├─────────────────────────────────────────────────────────────┤ │
|
||
│ │ 📱 Pixel 8 ✓ Connected │ │
|
||
│ │ Android 14 • WiFi (192.168.1.42) │ │
|
||
│ │ [Open App] │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ───────────────────────────────────────────────────────────────── │
|
||
│ │
|
||
│ NETWORK PREVIEW (WiFi) │
|
||
│ Devices on the same network can connect to: │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ http://192.168.1.100:8574 [Copy] │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────┐ │
|
||
│ │ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄│ │
|
||
│ │ █ █ █ █│ Scan with │
|
||
│ │ █ ███ █ █ ███ █│ Noodl Preview App │
|
||
│ │ █ █ █ █│ │
|
||
│ │ ▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀│ │
|
||
│ └─────────────────┘ │
|
||
│ │
|
||
│ [Download iOS Preview App] [Download Android Preview App] │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## Capacitor-Specific Nodes
|
||
|
||
### Core Plugin Nodes
|
||
|
||
#### Camera Node
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ 📷 Camera Capture [📱] [🖥️] │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ INPUTS │
|
||
│ ─────────────────────────────────────────────────────────────────── │
|
||
│ │
|
||
│ Capture ○──── (Signal - triggers capture) │
|
||
│ │
|
||
│ Source: [Camera ▾] │
|
||
│ ├─ Camera │
|
||
│ ├─ Photos Library │
|
||
│ └─ Prompt User │
|
||
│ │
|
||
│ Quality: [High ▾] │
|
||
│ ├─ Low (faster, smaller) │
|
||
│ ├─ Medium │
|
||
│ └─ High (best quality) │
|
||
│ │
|
||
│ Result Type: [Base64 ▾] │
|
||
│ ├─ Base64 (data URL) │
|
||
│ ├─ URI (file path) │
|
||
│ └─ Blob │
|
||
│ │
|
||
│ Direction: [Rear ▾] │
|
||
│ Correct Orientation: [✓] │
|
||
│ Allow Editing: [✓] │
|
||
│ Width: [1024 ] (0 = original) │
|
||
│ Height: [0 ] (0 = original) │
|
||
│ │
|
||
│ OUTPUTS │
|
||
│ ─────────────────────────────────────────────────────────────────── │
|
||
│ │
|
||
│ Photo ────○ (String - base64/URI) │
|
||
│ Format ────○ (String - "jpeg"/"png"/"gif") │
|
||
│ Captured ────○ (Signal - on success) │
|
||
│ Failed ────○ (Signal - on error) │
|
||
│ Error ────○ (String - error message) │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Implementation:**
|
||
|
||
```typescript
|
||
// packages/noodl-runtime/src/nodes/capacitor/CameraNode.ts
|
||
|
||
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
|
||
import { createNodeDefinition } from '../../nodeDefinition';
|
||
|
||
export const CameraNode = createNodeDefinition({
|
||
name: 'Camera Capture',
|
||
category: 'Device & Platform',
|
||
color: 'purple',
|
||
|
||
inputs: {
|
||
capture: { type: 'signal', displayName: 'Capture' },
|
||
source: {
|
||
type: 'enum',
|
||
displayName: 'Source',
|
||
default: 'camera',
|
||
enums: [
|
||
{ value: 'camera', label: 'Camera' },
|
||
{ value: 'photos', label: 'Photos Library' },
|
||
{ value: 'prompt', label: 'Prompt User' },
|
||
],
|
||
},
|
||
quality: {
|
||
type: 'number',
|
||
displayName: 'Quality',
|
||
default: 90,
|
||
min: 1,
|
||
max: 100,
|
||
},
|
||
resultType: {
|
||
type: 'enum',
|
||
displayName: 'Result Type',
|
||
default: 'base64',
|
||
enums: [
|
||
{ value: 'base64', label: 'Base64' },
|
||
{ value: 'uri', label: 'URI' },
|
||
],
|
||
},
|
||
direction: {
|
||
type: 'enum',
|
||
displayName: 'Direction',
|
||
default: 'rear',
|
||
enums: [
|
||
{ value: 'rear', label: 'Rear' },
|
||
{ value: 'front', label: 'Front' },
|
||
],
|
||
},
|
||
allowEditing: { type: 'boolean', displayName: 'Allow Editing', default: false },
|
||
width: { type: 'number', displayName: 'Width', default: 0 },
|
||
height: { type: 'number', displayName: 'Height', default: 0 },
|
||
},
|
||
|
||
outputs: {
|
||
photo: { type: 'string', displayName: 'Photo' },
|
||
format: { type: 'string', displayName: 'Format' },
|
||
captured: { type: 'signal', displayName: 'Captured' },
|
||
failed: { type: 'signal', displayName: 'Failed' },
|
||
error: { type: 'string', displayName: 'Error' },
|
||
},
|
||
|
||
targetCompatibility: ['capacitor', 'electron', 'web'],
|
||
|
||
async execute(inputs, outputs, context) {
|
||
try {
|
||
const sourceMap = {
|
||
camera: CameraSource.Camera,
|
||
photos: CameraSource.Photos,
|
||
prompt: CameraSource.Prompt,
|
||
};
|
||
|
||
const resultTypeMap = {
|
||
base64: CameraResultType.Base64,
|
||
uri: CameraResultType.Uri,
|
||
};
|
||
|
||
const photo = await Camera.getPhoto({
|
||
source: sourceMap[inputs.source],
|
||
resultType: resultTypeMap[inputs.resultType],
|
||
quality: inputs.quality,
|
||
allowEditing: inputs.allowEditing,
|
||
width: inputs.width || undefined,
|
||
height: inputs.height || undefined,
|
||
direction: inputs.direction === 'front' ? 'FRONT' : 'REAR',
|
||
});
|
||
|
||
if (inputs.resultType === 'base64') {
|
||
outputs.photo = `data:image/${photo.format};base64,${photo.base64String}`;
|
||
} else {
|
||
outputs.photo = photo.webPath;
|
||
}
|
||
|
||
outputs.format = photo.format;
|
||
outputs.captured.trigger();
|
||
|
||
} catch (err) {
|
||
outputs.error = err.message;
|
||
outputs.failed.trigger();
|
||
}
|
||
},
|
||
});
|
||
```
|
||
|
||
#### Geolocation Node
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ 📍 Geolocation [🌐] [📱] [🖥️] [🧩] │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ INPUTS │
|
||
│ ─────────────────────────────────────────────────────────────────── │
|
||
│ │
|
||
│ Get Position ○──── (Signal) │
|
||
│ Watch Position ○──── (Signal - start watching) │
|
||
│ Stop Watching ○──── (Signal) │
|
||
│ │
|
||
│ High Accuracy: [✓] (uses GPS, slower, more battery) │
|
||
│ Timeout (ms): [10000] │
|
||
│ Maximum Age (ms): [0 ] (0 = always fresh) │
|
||
│ │
|
||
│ OUTPUTS │
|
||
│ ─────────────────────────────────────────────────────────────────── │
|
||
│ │
|
||
│ Latitude ────○ (Number) │
|
||
│ Longitude ────○ (Number) │
|
||
│ Accuracy ────○ (Number - meters) │
|
||
│ Altitude ────○ (Number - meters, may be null) │
|
||
│ Speed ────○ (Number - m/s, may be null) │
|
||
│ Heading ────○ (Number - degrees, may be null) │
|
||
│ Timestamp ────○ (Date) │
|
||
│ │
|
||
│ Position Updated ────○ (Signal) │
|
||
│ Error ────○ (Signal) │
|
||
│ Error Message ────○ (String) │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### Push Notifications Node
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ 🔔 Push Notifications [📱] │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ INPUTS │
|
||
│ ─────────────────────────────────────────────────────────────────── │
|
||
│ │
|
||
│ Register ○──── (Signal - request permission) │
|
||
│ │
|
||
│ OUTPUTS │
|
||
│ ─────────────────────────────────────────────────────────────────── │
|
||
│ │
|
||
│ Token ────○ (String - device token) │
|
||
│ Registered ────○ (Signal) │
|
||
│ Registration Failed ────○ (Signal) │
|
||
│ │
|
||
│ Notification Received ────○ (Signal) │
|
||
│ Notification Title ────○ (String) │
|
||
│ Notification Body ────○ (String) │
|
||
│ Notification Data ────○ (Object - custom payload) │
|
||
│ Notification Action ────○ (String - action ID if tapped) │
|
||
│ │
|
||
│ Notification Tapped ────○ (Signal - user tapped notif) │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### Additional Capacitor Nodes
|
||
|
||
| Node | Category | Description |
|
||
|------|----------|-------------|
|
||
| **Haptics** | Device | Trigger haptic feedback (impact, notification, selection) |
|
||
| **Status Bar** | UI | Control status bar appearance (style, color, visibility) |
|
||
| **Keyboard** | UI | Show/hide keyboard, get keyboard height |
|
||
| **Share** | Social | Native share sheet for content |
|
||
| **App Launcher** | System | Open other apps, URLs, settings |
|
||
| **Device Info** | System | Get device model, OS version, battery, etc. |
|
||
| **Network** | System | Get connection type, monitor connectivity |
|
||
| **Splash Screen** | UI | Control splash screen (hide, show) |
|
||
| **Local Notifications** | Notifications | Schedule local notifications |
|
||
| **Biometric Auth** | Security | Face ID, Touch ID, fingerprint auth |
|
||
| **Clipboard** | Utility | Read/write clipboard |
|
||
| **Browser** | Navigation | Open in-app browser |
|
||
| **App State** | Lifecycle | Monitor foreground/background state |
|
||
|
||
## Project Export
|
||
|
||
### Export Flow
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ Export for Mobile [×] │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ PLATFORM │
|
||
│ ┌───────────────────────────────┐ ┌───────────────────────────────┐ │
|
||
│ │ [Apple Logo] │ │ [Android Logo] │ │
|
||
│ │ │ │ │ │
|
||
│ │ iOS │ │ Android │ │
|
||
│ │ ✓ Selected │ │ ☐ │ │
|
||
│ └───────────────────────────────┘ └───────────────────────────────┘ │
|
||
│ │
|
||
│ ─────────────────────────────────────────────────────────────────── │
|
||
│ │
|
||
│ APP CONFIGURATION │
|
||
│ │
|
||
│ App ID: │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ com.mycompany.myapp │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ ⓘ Must match your App Store / Play Store app ID │
|
||
│ │
|
||
│ App Name: │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ My Awesome App │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Version: Build Number: │
|
||
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
|
||
│ │ 1.0.0 │ │ 1 │ │
|
||
│ └─────────────────────────┘ └─────────────────────────┘ │
|
||
│ │
|
||
│ ▶ iOS Settings │
|
||
│ Team ID: [ABC123DEF4 ] │
|
||
│ Deployment Target: [iOS 14.0 ▾] │
|
||
│ │
|
||
│ ▶ Plugins │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ ☑ @capacitor/camera (Used by: Camera Capture) │ │
|
||
│ │ ☑ @capacitor/geolocation (Used by: Geolocation) │ │
|
||
│ │ ☑ @capacitor/push-notifications (Used by: Push Notif...) │ │
|
||
│ │ ☐ @capacitor/haptics (Not used) │ │
|
||
│ │ ☐ @capacitor/share (Not used) │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ▶ Permissions (auto-detected from node usage) │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ 📷 Camera Access │ │
|
||
│ │ "This app needs camera access to take photos" │ │
|
||
│ │ │ │
|
||
│ │ 📍 Location When In Use │ │
|
||
│ │ "This app needs your location to show nearby places" │ │
|
||
│ │ │ │
|
||
│ │ 🔔 Push Notifications │ │
|
||
│ │ (Configured in Apple Developer Portal) │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ─────────────────────────────────────────────────────────────────── │
|
||
│ │
|
||
│ OUTPUT │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ ○ Generate Xcode/Android Studio Project │ │
|
||
│ │ Full native project ready for building and customization │ │
|
||
│ │ │ │
|
||
│ │ ● Generate Build-Ready Package RECOMMENDED │ │
|
||
│ │ Minimal project, just run `npx cap build` │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ [Cancel] [Export for iOS] │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Generated Project Structure
|
||
|
||
```
|
||
my-app-capacitor/
|
||
├── package.json # Node.js package with Capacitor deps
|
||
├── capacitor.config.ts # Capacitor configuration
|
||
├── www/ # Built Noodl app (web assets)
|
||
│ ├── index.html
|
||
│ ├── main.js
|
||
│ ├── styles.css
|
||
│ └── assets/
|
||
├── ios/ # iOS Xcode project
|
||
│ ├── App/
|
||
│ │ ├── App.xcodeproj
|
||
│ │ ├── App/
|
||
│ │ │ ├── AppDelegate.swift
|
||
│ │ │ ├── Info.plist # Permissions, app config
|
||
│ │ │ └── Assets.xcassets # App icons
|
||
│ │ └── Podfile # iOS dependencies
|
||
│ └── Pods/
|
||
├── android/ # Android Studio project
|
||
│ ├── app/
|
||
│ │ ├── build.gradle
|
||
│ │ ├── src/main/
|
||
│ │ │ ├── AndroidManifest.xml
|
||
│ │ │ ├── java/.../MainActivity.java
|
||
│ │ │ └── res/ # Icons, splash screens
|
||
│ └── gradle/
|
||
└── README.md # Build instructions
|
||
```
|
||
|
||
### capacitor.config.ts
|
||
|
||
```typescript
|
||
import { CapacitorConfig } from '@capacitor/cli';
|
||
|
||
const config: CapacitorConfig = {
|
||
appId: 'com.mycompany.myapp',
|
||
appName: 'My Awesome App',
|
||
webDir: 'www',
|
||
|
||
// Development: connect to Noodl dev server
|
||
server: process.env.NODE_ENV === 'development' ? {
|
||
url: 'http://localhost:8574',
|
||
cleartext: true,
|
||
} : undefined,
|
||
|
||
plugins: {
|
||
Camera: {
|
||
// iOS requires these privacy descriptions
|
||
},
|
||
PushNotifications: {
|
||
presentationOptions: ['badge', 'sound', 'alert'],
|
||
},
|
||
},
|
||
|
||
ios: {
|
||
// iOS-specific configuration
|
||
},
|
||
|
||
android: {
|
||
// Android-specific configuration
|
||
},
|
||
};
|
||
|
||
export default config;
|
||
```
|
||
|
||
## Implementation Phases
|
||
|
||
### Phase B.1: Capacitor Bridge Integration (1 week)
|
||
|
||
**Goal:** Enable Noodl preview to run inside Capacitor WebView with hot-reload.
|
||
|
||
**Tasks:**
|
||
- [ ] Detect when running in Capacitor context (`window.Capacitor.isNativePlatform()`)
|
||
- [ ] Inject Capacitor bridge scripts in preview HTML
|
||
- [ ] Configure dev server for external access (CORS, network binding)
|
||
- [ ] Create "Capacitor Preview Mode" toggle in editor
|
||
- [ ] Document Xcode/Android Studio setup for live-reload
|
||
|
||
**Files to Create/Modify:**
|
||
```
|
||
packages/noodl-editor/src/frames/viewer-frame/src/views/
|
||
├── viewer.js # Modify: Add Capacitor bridge injection
|
||
└── capacitorBridge.js # New: Capacitor-specific preview logic
|
||
|
||
packages/noodl-editor/src/editor/src/views/EditorTopbar/
|
||
└── PreviewModeSelector.tsx # New: Preview mode dropdown component
|
||
```
|
||
|
||
### Phase B.2: Core Capacitor Nodes (2 weeks)
|
||
|
||
**Goal:** Implement essential device capability nodes.
|
||
|
||
**Week 1: High-priority nodes**
|
||
- [ ] Camera Capture node
|
||
- [ ] Geolocation node
|
||
- [ ] Push Notifications node
|
||
- [ ] Haptics node
|
||
|
||
**Week 2: Secondary nodes**
|
||
- [ ] Share node
|
||
- [ ] Status Bar node
|
||
- [ ] Device Info node
|
||
- [ ] Network Status node
|
||
- [ ] App State node
|
||
|
||
**Files to Create:**
|
||
```
|
||
packages/noodl-runtime/src/nodes/capacitor/
|
||
├── index.ts # Export all Capacitor nodes
|
||
├── CameraNode.ts
|
||
├── GeolocationNode.ts
|
||
├── PushNotificationsNode.ts
|
||
├── HapticsNode.ts
|
||
├── ShareNode.ts
|
||
├── StatusBarNode.ts
|
||
├── DeviceInfoNode.ts
|
||
├── NetworkNode.ts
|
||
├── AppStateNode.ts
|
||
└── _mocks/ # Web fallback implementations
|
||
├── cameraMock.ts
|
||
├── geolocationMock.ts
|
||
└── ...
|
||
```
|
||
|
||
### Phase B.3: Simulator/Device Launch (1 week)
|
||
|
||
**Goal:** One-click launch to iOS Simulator or Android Emulator.
|
||
|
||
**Tasks:**
|
||
- [ ] Detect installed simulators (`xcrun simctl list`)
|
||
- [ ] Detect installed Android emulators (`emulator -list-avds`)
|
||
- [ ] Create simulator selection UI
|
||
- [ ] Implement launch command execution
|
||
- [ ] Display QR code for physical device connection
|
||
|
||
**Files to Create:**
|
||
```
|
||
packages/noodl-editor/src/editor/src/views/panels/
|
||
└── CapacitorPreview/
|
||
├── SimulatorList.tsx
|
||
├── DeviceConnection.tsx
|
||
└── QRCodeModal.tsx
|
||
```
|
||
|
||
### Phase B.4: Export Pipeline (1 week)
|
||
|
||
**Goal:** Generate production-ready Capacitor project.
|
||
|
||
**Tasks:**
|
||
- [ ] Build web assets to `www/` folder
|
||
- [ ] Generate `capacitor.config.ts`
|
||
- [ ] Generate `package.json` with correct dependencies
|
||
- [ ] Auto-detect required plugins from node usage
|
||
- [ ] Generate platform-specific permission strings
|
||
- [ ] Create iOS Xcode project scaffolding
|
||
- [ ] Create Android Studio project scaffolding
|
||
- [ ] Generate README with build instructions
|
||
|
||
**Files to Create:**
|
||
```
|
||
packages/noodl-editor/src/editor/src/export/
|
||
├── capacitor/
|
||
│ ├── CapacitorExporter.ts # Main export orchestrator
|
||
│ ├── configGenerator.ts # Generate capacitor.config.ts
|
||
│ ├── packageGenerator.ts # Generate package.json
|
||
│ ├── pluginDetector.ts # Detect required plugins from graph
|
||
│ ├── permissionGenerator.ts # Generate iOS Info.plist entries
|
||
│ ├── iosProjectGenerator.ts # Scaffold iOS project
|
||
│ └── androidProjectGenerator.ts # Scaffold Android project
|
||
└── templates/
|
||
└── capacitor/
|
||
├── package.json.template
|
||
├── capacitor.config.ts.template
|
||
└── ios/
|
||
└── Info.plist.template
|
||
```
|
||
|
||
## Web Fallback Behavior
|
||
|
||
When Capacitor nodes are used in web preview or web deployment:
|
||
|
||
| Node | Web Fallback Behavior |
|
||
|------|----------------------|
|
||
| **Camera** | Uses `navigator.mediaDevices.getUserMedia()` |
|
||
| **Geolocation** | Uses `navigator.geolocation` |
|
||
| **Push Notifications** | Shows warning, suggests Web Push API setup |
|
||
| **Haptics** | Uses `navigator.vibrate()` (limited support) |
|
||
| **Share** | Uses Web Share API (Chrome, Safari) |
|
||
| **Status Bar** | No-op (not applicable to web) |
|
||
| **Device Info** | Returns `navigator.userAgent` parsed info |
|
||
| **Network** | Uses `navigator.connection` |
|
||
|
||
```typescript
|
||
// Example: Camera fallback implementation
|
||
async function capturePhotoWeb(options: CameraOptions): Promise<Photo> {
|
||
// Check if we're in a native Capacitor context
|
||
if (window.Capacitor?.isNativePlatform()) {
|
||
return Camera.getPhoto(options);
|
||
}
|
||
|
||
// Web fallback using MediaDevices API
|
||
const stream = await navigator.mediaDevices.getUserMedia({
|
||
video: {
|
||
facingMode: options.direction === 'front' ? 'user' : 'environment',
|
||
width: options.width || undefined,
|
||
height: options.height || undefined,
|
||
}
|
||
});
|
||
|
||
// Create video element to capture frame
|
||
const video = document.createElement('video');
|
||
video.srcObject = stream;
|
||
await video.play();
|
||
|
||
// Draw frame to canvas
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = video.videoWidth;
|
||
canvas.height = video.videoHeight;
|
||
const ctx = canvas.getContext('2d')!;
|
||
ctx.drawImage(video, 0, 0);
|
||
|
||
// Stop stream
|
||
stream.getTracks().forEach(track => track.stop());
|
||
|
||
// Convert to base64
|
||
const dataUrl = canvas.toDataURL('image/jpeg', options.quality / 100);
|
||
|
||
return {
|
||
base64String: dataUrl.split(',')[1],
|
||
format: 'jpeg',
|
||
webPath: dataUrl,
|
||
};
|
||
}
|
||
```
|
||
|
||
## Testing Strategy
|
||
|
||
### Unit Tests
|
||
|
||
```typescript
|
||
describe('CameraNode', () => {
|
||
describe('web fallback', () => {
|
||
it('should use MediaDevices API when not in native context', async () => {
|
||
const mockGetUserMedia = jest.fn().mockResolvedValue(mockStream);
|
||
navigator.mediaDevices.getUserMedia = mockGetUserMedia;
|
||
|
||
const node = new CameraNode();
|
||
node.setInput('source', 'camera');
|
||
await node.executeCapture();
|
||
|
||
expect(mockGetUserMedia).toHaveBeenCalledWith({
|
||
video: expect.any(Object),
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('native context', () => {
|
||
beforeEach(() => {
|
||
window.Capacitor = { isNativePlatform: () => true };
|
||
});
|
||
|
||
it('should use Capacitor Camera plugin', async () => {
|
||
const mockGetPhoto = jest.fn().mockResolvedValue(mockPhoto);
|
||
Camera.getPhoto = mockGetPhoto;
|
||
|
||
const node = new CameraNode();
|
||
await node.executeCapture();
|
||
|
||
expect(mockGetPhoto).toHaveBeenCalled();
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
### Integration Tests
|
||
|
||
```typescript
|
||
describe('Capacitor Export', () => {
|
||
it('should detect required plugins from node graph', async () => {
|
||
const project = createTestProject([
|
||
{ type: 'Camera Capture' },
|
||
{ type: 'Geolocation' },
|
||
{ type: 'Text' }, // Not a Capacitor node
|
||
]);
|
||
|
||
const detector = new PluginDetector(project);
|
||
const plugins = detector.getRequiredPlugins();
|
||
|
||
expect(plugins).toEqual([
|
||
'@capacitor/camera',
|
||
'@capacitor/geolocation',
|
||
]);
|
||
});
|
||
|
||
it('should generate valid capacitor.config.ts', async () => {
|
||
const exporter = new CapacitorExporter(mockProject);
|
||
const config = await exporter.generateConfig();
|
||
|
||
expect(config).toContain("appId: 'com.test.app'");
|
||
expect(config).toContain("appName: 'Test App'");
|
||
});
|
||
});
|
||
```
|
||
|
||
### Manual Testing Checklist
|
||
|
||
- [ ] Create new Capacitor-target project
|
||
- [ ] Add Camera node, verify web fallback works
|
||
- [ ] Enable Capacitor preview mode
|
||
- [ ] Launch iOS Simulator with live-reload
|
||
- [ ] Take photo in simulator, verify it appears in Noodl
|
||
- [ ] Export project for iOS
|
||
- [ ] Open in Xcode, build successfully
|
||
- [ ] Run on physical iPhone, verify all features work
|
||
|
||
## Success Criteria
|
||
|
||
| Criteria | Target |
|
||
|----------|--------|
|
||
| Hot-reload latency | < 2 seconds from save to device update |
|
||
| Export time | < 30 seconds for complete Capacitor project |
|
||
| Xcode build success | First-time build succeeds without manual fixes |
|
||
| Plugin detection accuracy | 100% of used plugins detected automatically |
|
||
| Web fallback coverage | All Capacitor nodes have functional web fallbacks |
|
||
|
||
## Future Enhancements
|
||
|
||
### Phase B+ Features (Post-MVP)
|
||
|
||
1. **Capacitor Plugins Marketplace** - Browse and install community plugins
|
||
2. **Native UI Components** - Use platform-native UI (iOS UIKit, Android Material)
|
||
3. **Background Tasks** - Run code when app is backgrounded
|
||
4. **Deep Linking** - Handle custom URL schemes
|
||
5. **In-App Purchases** - Integrate with App Store / Play Store purchases
|
||
6. **App Store Deployment** - One-click submit to stores (via Fastlane)
|
||
|
||
### Advanced Native Integration
|
||
|
||
For users who need more native control:
|
||
|
||
```typescript
|
||
// Custom Capacitor Plugin Node
|
||
// Allows calling any Capacitor plugin method
|
||
|
||
interface CustomPluginNodeInputs {
|
||
pluginName: string; // e.g., "@capacitor/camera"
|
||
methodName: string; // e.g., "getPhoto"
|
||
options: object; // Method parameters
|
||
}
|
||
|
||
// This enables using ANY Capacitor plugin, even community ones
|
||
```
|