From b50bd27eea26797df6c43622792cc05b3d1239a3 Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Wed, 11 Feb 2026 10:53:13 +0300 Subject: [PATCH 01/25] fix lint errors in the formplyer Signed-off-by: Jessie Ssebuliba --- formulus-formplayer/src/services/ExtensionsLoader.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/formulus-formplayer/src/services/ExtensionsLoader.ts b/formulus-formplayer/src/services/ExtensionsLoader.ts index 14884ec14..ef3291969 100644 --- a/formulus-formplayer/src/services/ExtensionsLoader.ts +++ b/formulus-formplayer/src/services/ExtensionsLoader.ts @@ -44,6 +44,7 @@ export interface LoadedRenderer { */ export interface ExtensionLoadResult { renderers: JsonFormsRendererRegistryEntry[]; + // Functions with explicit signature for type safety functions: Map any>; definitions: Record; errors: Array<{ type: string; message: string; details?: any }>; @@ -189,8 +190,7 @@ async function loadRenderer( async function loadFunction( metadata: ExtensionFunctionMetadata, basePath: string, - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -): Promise { +): Promise<((...args: any[]) => any) | null> { try { // Construct module path const modulePath = basePath From ef922799a14dbfdbe1a456a83776e1bc19dbc5a8 Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Tue, 17 Feb 2026 17:19:52 +0300 Subject: [PATCH 02/25] feat: secure custom question type loading via source extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace dynamic import() of file:// URIs with a sandboxed evaluation approach for custom question type modules. Security: - Add CustomQuestionTypeScanner (RN side) that reads index.js files as strings and screens them against a blocklist (fetch, XMLHttpRequest, eval, document.cookie, localStorage, etc.) - Rewrite CustomQuestionTypeLoader (WebView side) to evaluate source in a scoped sandbox via new Function(), exposing only React and MUI - Manifest shape changed from { modulePath: string } to { source: string } New files: - formulus/src/services/CustomQuestionTypeScanner.ts - formulus-formplayer/src/services/CustomQuestionTypeLoader.ts (rewritten) - formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts - formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx - formulus-formplayer/src/types/CustomQuestionTypeContract.ts - formulus-formplayer/docs/custom-question-types-architecture.md Modified files: - formulus/src/components/FormplayerModal.tsx (calls scanner) - FormulusInterfaceDefinition.ts (both projects, modulePath → source) - formulus-formplayer/src/App.tsx (orchestration) Signed-off-by: Jessie Ssebuliba --- .../custom-question-types-architecture.md | 404 ++++++++++++++++++ formulus-formplayer/src/App.tsx | 45 +- .../renderers/CustomQuestionTypeAdapter.tsx | 127 ++++++ .../src/services/CustomQuestionTypeLoader.ts | 163 +++++++ .../services/CustomQuestionTypeRegistry.ts | 57 +++ .../src/types/CustomQuestionTypeContract.ts | 67 +++ .../src/types/FormulusInterfaceDefinition.ts | 4 + formulus/src/components/FormplayerModal.tsx | 23 +- .../src/services/CustomQuestionTypeScanner.ts | 182 ++++++++ .../webview/FormulusInterfaceDefinition.ts | 4 + 10 files changed, 1074 insertions(+), 2 deletions(-) create mode 100644 formulus-formplayer/docs/custom-question-types-architecture.md create mode 100644 formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx create mode 100644 formulus-formplayer/src/services/CustomQuestionTypeLoader.ts create mode 100644 formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts create mode 100644 formulus-formplayer/src/types/CustomQuestionTypeContract.ts create mode 100644 formulus/src/services/CustomQuestionTypeScanner.ts diff --git a/formulus-formplayer/docs/custom-question-types-architecture.md b/formulus-formplayer/docs/custom-question-types-architecture.md new file mode 100644 index 000000000..bb53d1cab --- /dev/null +++ b/formulus-formplayer/docs/custom-question-types-architecture.md @@ -0,0 +1,404 @@ +# Custom Question Types — Architecture & Flow + +--- + +## File Structure + +``` +formulus-formplayer/src/ (FORMPLAYER — runs in WebView) +├── types/ +│ ├── CustomQuestionTypeContract.ts ← 1. The contract authors code against +│ └── FormulusInterfaceDefinition.ts ← 2. FormInitData (carries the manifest) +│ +├── services/ +│ ├── CustomQuestionTypeLoader.ts ← 3. Sandboxed evaluation of source strings +│ └── CustomQuestionTypeRegistry.ts ← 4. Auto-generates testers + renderer entries +│ +├── renderers/ +│ └── CustomQuestionTypeAdapter.tsx ← 5. Bridges ControlProps → CustomQuestionTypeProps +│ +└── App.tsx ← 6. Orchestrates everything + +formulus/src/ (FORMULUS — runs in React Native) +├── services/ +│ └── CustomQuestionTypeScanner.ts ← Reads files, screens against blocklist +│ +└── components/ + └── FormplayerModal.tsx ← Calls scanner, passes source in FormInitData +``` + +### Author's Side (custom_app) + +``` +custom_app/ +└── question_types/ + ├── x-ranking/ + │ └── index.js ← default export: React component + ├── x-dynamicEnum/ + │ └── index.js + └── x-custom-text/ + └── index.js +``` + +--- + +## Security Model — Source Extraction + +Custom question type JS files could contain malicious code. Instead of letting the WebView +`import()` arbitrary scripts (which would give them full access to fetch, DOM, localStorage, etc.), +we use a **source extraction** approach with two layers of defense: + +| Layer | Where | What it does | +|-------|-------|-------------| +| **Static blocklist** | RN side (`CustomQuestionTypeScanner`) | Rejects code containing dangerous patterns before it reaches the WebView | +| **Scoped evaluation** | WebView (`CustomQuestionTypeLoader`) | `new Function()` sandbox — code can only access React and MUI, nothing else | + +### Blocked Patterns (RN-side screening) + +``` +fetch( — network requests +XMLHttpRequest — network requests +WebSocket — persistent connections +eval( — dynamic code execution +new Function( — dynamic code execution +document.cookie — cookie access +localStorage — storage access +sessionStorage — storage access +indexedDB — database access +navigator.sendBeacon — data exfiltration +importScripts( — script injection +``` + +### Scoped Sandbox (WebView-side evaluation) + +```javascript +// Instead of: import("file:///path/to/index.js") +// We do: +const factory = new Function( + 'module', 'exports', 'React', 'MaterialUI', + sourceString // ← sent from RN as a string, not a file path +); + +// Custom code CAN access: React, MaterialUI, module, exports +// Custom code CANNOT access: fetch, document, localStorage, window, etc. +``` + +--- + +## How Module Loading Works + +### 1. Device Storage + +When the custom_app archive is unzipped, files land on the device filesystem: + +``` +/data/.../Documents/app/ +├── forms/ +│ ├── hh_hut/schema.json +│ ├── hh_person/schema.json +│ ├── p_focal/schema.json +│ └── ... +└── question_types/ + ├── x-ranking/index.js ← pairwise Elo ranking UI + ├── x-dynamicEnum/index.js ← dynamic choice list from DB queries + └── x-custom-text/index.js ← enhanced text input +``` + +### 2. Formulus RN Scans, Reads & Screens + +`CustomQuestionTypeScanner.ts` scans `question_types/`, reads each `index.js` as a raw string, +and screens it against the blocklist: + +```typescript +// In CustomQuestionTypeScanner.ts +const questionTypesDir = `${customAppPath}/question_types`; +const folders = await RNFS.readDir(questionTypesDir); + +for (const folder of folders) { + if (folder.isDirectory()) { + const source = await RNFS.readFile(`${folder.path}/index.js`, 'utf8'); + + // Screen against blocklist + const violation = screenSource(source); + if (violation) { + errors.push({ name: folder.name, error: `Blocked: ${violation}` }); + continue; + } + + // Source is clean — include it + custom_types[folder.name] = { source }; + } +} +``` + +**Sample manifest** (source strings, not file paths): + +```json +{ + "custom_types": { + "x-ranking": { + "source": "(function() { 'use strict'; ... module.exports = RankingRenderer; })()" + }, + "x-dynamicEnum": { + "source": "(function() { 'use strict'; ... module.exports = DynamicEnumControl; })()" + }, + "x-custom-text": { + "source": "(function() { 'use strict'; ... module.exports = CustomTextRenderer; })()" + } + } +} +``` + +### 3. FormInitData Carries the Source Strings + +In `FormplayerModal.tsx`, `initializeForm()` calls the scanner and includes the result: + +```typescript +const customAppPath = RNFS.DocumentDirectoryPath + '/app'; + +// Scan and screen custom question types +const scanResult = await scanCustomQuestionTypes(customAppPath); +if (scanResult.errors.length > 0) { + console.warn('Some custom question types failed screening:', scanResult.errors); +} + +const formInitData = { + formType: formType.id, + observationId, + params: formParams, + savedData: existingObservationData || {}, + formSchema: formType.schema, + uiSchema: formType.uiSchema ?? {}, + extensions, + customQuestionTypes: { + custom_types: scanResult.custom_types, + }, +} as FormInitData; +``` + +### 4. WebView Receives & Evaluates in Sandbox + +`FormulusWebViewHandler.sendFormInit()` serializes the `FormInitData` and injects it into +the WebView. Then `CustomQuestionTypeLoader.ts` evaluates each source in a scoped sandbox: + +```typescript +// CustomQuestionTypeLoader.ts — evaluateModuleInSandbox() +const exports = {}; +const moduleObj = { exports }; + +const factory = new Function( + 'module', 'exports', 'React', 'MaterialUI', + meta.source, +); + +factory(moduleObj, exports, React, MaterialUI); + +// Extract only the component +const component = moduleObj.exports.default ?? moduleObj.exports; +``` + +### 5. Registry & Rendering + +`CustomQuestionTypeRegistry.ts` takes each loaded component and: +- Auto-generates a tester: `rankWith(6, schemaMatches(s => s.format === name))` +- Creates a renderer entry via `CustomQuestionTypeAdapter.tsx` +- Registers the format with AJV: `ajv.addFormat('x-ranking', () => true)` + +--- + +## Flow Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ DEVICE STORAGE (after custom_app unzip) │ +│ │ +│ /Documents/app/question_types/x-ranking/index.js │ +│ /Documents/app/question_types/x-dynamicEnum/index.js │ +│ /Documents/app/question_types/x-custom-text/index.js │ +└────────────────────────┬────────────────────────────────────┘ + │ RNFS.readFile() → string + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ FORMULUS RN — CustomQuestionTypeScanner │ +│ │ +│ 1. Reads each index.js as a raw string │ +│ 2. Screens against blocklist (fetch, eval, etc.) │ +│ 3. Builds manifest with source strings: │ +│ { "x-ranking": { source: "..." } } │ +│ 4. Rejected modules → logged as warnings │ +└────────────────────────┬────────────────────────────────────┘ + │ FormInitData.customQuestionTypes + │ sendFormInit() → injectJavaScript() + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ FORMPLAYER WEBVIEW — App.tsx │ +│ │ +│ initializeForm() reads initData.customQuestionTypes │ +│ calls loadCustomQuestionTypes(manifest) │ +└────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ CustomQuestionTypeLoader.ts — SANDBOX │ +│ │ +│ For each entry in manifest.custom_types: │ +│ new Function('module','exports','React','MaterialUI', │ +│ source) │ +│ Extracts module.exports.default (React component) │ +│ Validates it's a function │ +│ ❌ No access to: fetch, document, localStorage, etc. │ +└────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ CustomQuestionTypeRegistry.ts │ +│ │ +│ For each loaded component: │ +│ Auto-generates a tester: │ +│ rankWith(6, schemaMatches(s => s.format === name)) │ +│ Creates renderer entry via adapter │ +└────────────────────────┬────────────────────────────────────┘ + │ + ┌──────────┴──────────┐ + ▼ ▼ +┌──────────────────────┐ ┌──────────────────────────────────┐ +│ AJV Registration │ │ JsonForms Renderers Array │ +│ │ │ │ +│ ajv.addFormat( │ │ [ │ +│ 'x-ranking', │ │ ...builtInRenderers, │ +│ () => true │ │ ...customTypeRenderers, ← NEW │ +│ ) │ │ ] │ +│ │ │ │ +│ Prevents AJV from │ │ Testers run top-to-bottom, │ +│ rejecting unknown │ │ highest rank wins │ +│ format strings │ │ │ +└──────────────────────┘ └───────────────┬──────────────────┘ + │ at render time + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ CustomQuestionTypeAdapter.tsx │ +│ │ +│ JSON Forms ControlProps → CustomQuestionTypeProps │ +│ ───────────────────── ──────────────────────── │ +│ data value │ +│ handleChange(path, val) onChange(val) │ +│ errors (string) validation { error, msg } │ +│ schema['x-config'] config │ +│ enabled enabled │ +│ path fieldPath │ +│ label, description label, description │ +│ │ +│ Wraps in: QuestionShell + ErrorBoundary │ +└────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Author's Component │ +│ │ +│ Receives only: { value, config, onChange, validation, ... }│ +│ No JSON Forms knowledge needed. │ +│ Crash-safe via ErrorBoundary. │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Schema Examples (based on AnthroCollect forms) + +### Ranking Question + +Used in `p_focal` — pairwise Elo ranking of people by social attributes: + +```json +{ + "ranking_result": { + "type": "object", + "format": "x-ranking", + "x-config": { + "sexFilter": "female", + "hardLimit": 250 + } + } +} +``` + +### Dynamic Enum Question + +Used across many forms — dropdown choices populated from database queries: + +```json +{ + "selected_person": { + "type": "string", + "format": "x-dynamicEnum", + "x-config": { + "query": "p_consent", + "params": { + "scope": "{{data.scope}}" + }, + "valueField": "observationId", + "labelField": "data.name" + } + } +} +``` + +### Custom Text Question + +Enhanced text input with configurable multiline and placeholder: + +```json +{ + "notes": { + "type": "string", + "format": "x-custom-text", + "maxLength": 500, + "x-config": { + "placeholder": "Enter field notes...", + "helperText": "Describe any notable observations" + } + } +} +``` + +**What happens for each:** + +1. `format: "x-ranking"` → tester matches → the ranking renderer is used +2. `x-config` → passed as `props.config` to the author's component +3. Standard JSON Schema keywords (`type`, `maxLength`, etc.) → validated by AJV as normal +4. AJV doesn't reject the custom format strings because we registered them + +--- + +## Implementation Plan (completed) + +All changes below have been implemented. + +### Formulus RN Side + +| File | Change | +|------|--------| +| `FormulusInterfaceDefinition.ts` | `modulePath` → `source` in `FormInitData.customQuestionTypes` | +| `CustomQuestionTypeScanner.ts` | **NEW** — scans, reads, screens question type modules | +| `FormplayerModal.tsx` | Calls scanner, passes source strings in `FormInitData` | + +### FormPlayer WebView Side + +| File | Change | +|------|--------| +| `FormulusInterfaceDefinition.ts` | `modulePath` → `source` (mirror) | +| `CustomQuestionTypeContract.ts` | `modulePath` → `source` in `CustomQuestionTypeManifest` | +| `CustomQuestionTypeLoader.ts` | Rewritten: `import()` → `new Function()` sandbox | + +### Key Files — Full Reference + +| File | Role | Key Export | +|------|------|-----------| +| `CustomQuestionTypeScanner.ts` (RN) | Reads & screens modules | `scanCustomQuestionTypes()` | +| `CustomQuestionTypeContract.ts` | Defines what authors receive | `CustomQuestionTypeProps` | +| `CustomQuestionTypeLoader.ts` | Sandboxed evaluation | `loadCustomQuestionTypes()` | +| `CustomQuestionTypeRegistry.ts` | Creates JsonForms entries | `registerCustomQuestionTypes()` | +| `CustomQuestionTypeAdapter.tsx` | Props bridge + error isolation | `createCustomQuestionTypeRenderer()` | +| `FormulusInterfaceDefinition.ts` | Carries source from RN → WebView | `FormInitData` | +| `App.tsx` | Orchestrates load → register → render | `initializeForm()` | +| `FormplayerModal.tsx` (RN) | Builds FormInitData, sends to WebView | `initializeForm()` | diff --git a/formulus-formplayer/src/App.tsx b/formulus-formplayer/src/App.tsx index cc8192e19..0de7ceec1 100644 --- a/formulus-formplayer/src/App.tsx +++ b/formulus-formplayer/src/App.tsx @@ -75,6 +75,7 @@ import DraftSelector from './components/DraftSelector'; import { loadExtensions } from './services/ExtensionsLoader'; import { getBuiltinExtensions } from './builtinExtensions'; import { FormEvaluationProvider } from './FormEvaluationContext'; +import { loadCustomQuestionTypes } from './services/CustomQuestionTypeLoader'; // Import development dependencies (Vite will tree-shake these in production) import { webViewMock } from './mocks/webview-mock'; @@ -281,6 +282,11 @@ function App() { const [extensionDefinitions, setExtensionDefinitions] = useState< Record >({}); + // Custom question type renderers (loaded from custom_app) + const [customTypeRenderers, setCustomTypeRenderers] = useState< + JsonFormsRendererRegistryEntry[] + >([]); + const [customTypeFormats, setCustomTypeFormats] = useState([]); // Reference to the FormulusClient instance and loading state const formulusClient = useRef(FormulusClient.getInstance()); @@ -381,6 +387,32 @@ function App() { console.log('[Formplayer] Using only built-in extensions'); } + // Load custom question types if provided + const customQTManifest = initData.customQuestionTypes; + if (customQTManifest) { + try { + const customQTResult = await loadCustomQuestionTypes(customQTManifest); + setCustomTypeRenderers(customQTResult.renderers); + setCustomTypeFormats(customQTResult.formats); + console.log( + `[Formplayer] Loaded ${customQTResult.renderers.length} custom question type(s)`, + ); + if (customQTResult.errors.length > 0) { + console.warn( + '[Formplayer] Custom question type loading errors:', + customQTResult.errors, + ); + } + } catch (error) { + console.error('[Formplayer] Failed to load custom question types:', error); + setCustomTypeRenderers([]); + setCustomTypeFormats([]); + } + } else { + setCustomTypeRenderers([]); + setCustomTypeFormats([]); + } + if (!formSchema) { console.warn( 'formSchema was not provided. Form rendering might fail or be incomplete.', @@ -799,6 +831,16 @@ function App() { return typeof data === 'string' && dateRegex.test(data); }); + // Register custom question type formats with AJV + if (customTypeFormats.length > 0) { + customTypeFormats.forEach((fmt) => { + instance.addFormat(fmt, () => true); + }); + console.log( + `[Formplayer] Registered ${customTypeFormats.length} custom format(s) with AJV`, + ); + } + // Add extension definitions to AJV for $ref support if (Object.keys(extensionDefinitions).length > 0) { // Add each definition individually so $ref can reference them @@ -808,7 +850,7 @@ function App() { } return instance; - }, [extensionDefinitions]); + }, [extensionDefinitions, customTypeFormats]); // Create dynamic theme based on dark mode preference and custom app colors. // When a custom app provides themeColors, they override the default palette @@ -986,6 +1028,7 @@ function App() { ...shellMaterialRenderers, ...materialRenderers, ...customRenderers, + ...customTypeRenderers, // Custom question types from custom_app ...extensionRenderers, // Extension renderers (highest priority) ]} cells={materialCells} diff --git a/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx b/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx new file mode 100644 index 000000000..cf2a52fae --- /dev/null +++ b/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx @@ -0,0 +1,127 @@ +/** + * CustomQuestionTypeAdapter.tsx + * + * Bridges JSON Forms ControlProps → CustomQuestionTypeProps. + * Wraps every custom question type in QuestionShell + ErrorBoundary + * so that form authors get consistent styling and crash isolation. + */ + +import React, { Component, type ErrorInfo, type ReactNode } from 'react'; +import { withJsonFormsControlProps } from '@jsonforms/react'; +import type { ControlProps } from '@jsonforms/core'; +import QuestionShell from '../components/QuestionShell'; +import type { CustomQuestionTypeProps } from '../types/CustomQuestionTypeContract'; + +// --------------------------------------------------------------------------- +// Error Boundary — catches crashes in custom components +// --------------------------------------------------------------------------- + +interface ErrorBoundaryProps { + formatName: string; + children: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +class CustomQuestionErrorBoundary extends Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + state: ErrorBoundaryState = { hasError: false, error: null }; + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, info: ErrorInfo): void { + console.error( + `[CustomQuestionType] "${this.props.formatName}" crashed:`, + error, + info.componentStack, + ); + } + + render() { + if (this.state.hasError) { + return ( +
+ Custom question type "{this.props.formatName}" failed +
+ {this.state.error?.message} +
+ ); + } + return this.props.children; + } +} + +// --------------------------------------------------------------------------- +// Adapter — maps ControlProps → CustomQuestionTypeProps +// --------------------------------------------------------------------------- + +/** + * Creates a JSON Forms renderer component for a given custom question type. + * + * @param formatName - The format string (e.g., "x-rating-stars") + * @param CustomComponent - The author's React component + */ +export function createCustomQuestionTypeRenderer( + formatName: string, + CustomComponent: React.ComponentType, +): React.ComponentType { + const AdapterInner: React.FC = ({ + data, + handleChange, + path, + schema, + errors, + enabled, + label, + description, + required, + }) => { + // Build the simplified props for the custom component + const customProps: CustomQuestionTypeProps = { + value: data, + config: (schema as Record)?.['x-config'] as Record ?? {}, + onChange: (newValue: unknown) => handleChange(path, newValue), + validation: { + error: Boolean(errors && errors.length > 0), + message: errors ?? '', + }, + enabled: enabled ?? true, + fieldPath: path, + label: label ?? '', + description: description, + }; + + return ( + + + + + + ); + }; + + AdapterInner.displayName = `CustomQuestionType(${formatName})`; + + // Wrap with JSON Forms HOC + return withJsonFormsControlProps(AdapterInner); +} diff --git a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts new file mode 100644 index 000000000..54d3b82fe --- /dev/null +++ b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts @@ -0,0 +1,163 @@ +/** + * CustomQuestionTypeLoader.ts + * + * Loads custom question type components from source strings provided by the + * Formulus RN side. Instead of dynamically importing files from the filesystem, + * this loader evaluates each module's source in a scoped sandbox using + * `new Function()`, which restricts what the code can access. + * + * Security layers: + * 1. RN-side static blocklist (in CustomQuestionTypeScanner) rejects dangerous patterns + * 2. Scoped evaluation here only exposes React — no fetch, document, localStorage, etc. + * + * This loader: + * 1. Iterates over the manifest + * 2. Evaluates each source string in a scoped sandbox + * 3. Extracts and validates the default export (must be a React component function) + * 4. Passes all loaded components to the registry + * 5. Returns renderer entries + format strings for AJV registration + */ + +import type { JsonFormsRendererRegistryEntry } from '@jsonforms/core'; +import type { + CustomQuestionTypeManifest, + CustomQuestionTypeProps, +} from '../types/CustomQuestionTypeContract'; +import { registerCustomQuestionTypes } from './CustomQuestionTypeRegistry'; +import type React from 'react'; + +export interface CustomQuestionTypeLoadResult { + /** JSON Forms renderer entries ready to be merged into the renderers array */ + renderers: JsonFormsRendererRegistryEntry[]; + /** Format strings that need to be registered with AJV */ + formats: string[]; + /** Any errors that occurred during loading */ + errors: Array<{ format: string; error: string }>; +} + +/** + * Evaluate a module source string in a scoped sandbox. + * + * The code only has access to the variables we explicitly pass in: + * - module / exports (CommonJS-style export mechanism) + * - React (so the component can use createElement, hooks, etc.) + * + * Dangerous globals (fetch, XMLHttpRequest, document, localStorage, etc.) + * are NOT available in this scope. + */ +function evaluateModuleInSandbox( + source: string, + formatName: string, +): React.ComponentType { + const exports: Record = {}; + const moduleObj = { exports }; + + // Get React from the global scope (it's available in the WebView) + const ReactLib = (window as unknown as Record).React; + if (!ReactLib) { + throw new Error('React is not available in the global scope'); + } + + // Get MUI from the global scope (custom components may use Material UI) + const MUILib = (window as unknown as Record).MaterialUI; + + try { + // Create a factory function with a restricted scope. + // The code can only access: module, exports, React, MaterialUI + // It CANNOT access: fetch, XMLHttpRequest, document, localStorage, etc. + const factory = new Function( + 'module', + 'exports', + 'React', + 'MaterialUI', + source, + ); + + factory(moduleObj, exports, ReactLib, MUILib); + } catch (err) { + throw new Error( + `Failed to evaluate module source: ${err instanceof Error ? err.message : String(err)}`, + ); + } + + // Extract the component from exports (support both default and module.exports patterns) + const component = (moduleObj.exports as Record).default ?? moduleObj.exports; + + if (typeof component !== 'function') { + throw new Error( + `Module "${formatName}" does not export a valid React component. ` + + `Expected a function, got ${typeof component}. ` + + `Make sure your module uses module.exports = Component or exports.default = Component.`, + ); + } + + return component as React.ComponentType; +} + +/** + * Load custom question types from a manifest containing source strings. + * + * @param manifest - The manifest describing available custom question types (with source code) + * @returns Loaded renderers, format strings, and any errors + */ +export async function loadCustomQuestionTypes( + manifest: CustomQuestionTypeManifest, +): Promise { + const result: CustomQuestionTypeLoadResult = { + renderers: [], + formats: [], + errors: [], + }; + + if (!manifest?.custom_types || Object.keys(manifest.custom_types).length === 0) { + console.log('[CustomQuestionTypeLoader] No custom question types in manifest'); + return result; + } + + const loadedComponents = new Map< + string, + React.ComponentType + >(); + + for (const [formatName, meta] of Object.entries(manifest.custom_types)) { + try { + console.log( + `[CustomQuestionTypeLoader] Evaluating "${formatName}" (${meta.source.length} bytes)`, + ); + + // Evaluate the source in a scoped sandbox + const component = evaluateModuleInSandbox(meta.source, formatName); + + loadedComponents.set(formatName, component); + result.formats.push(formatName); + + console.log( + `[CustomQuestionTypeLoader] Successfully loaded "${formatName}"`, + ); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + console.error( + `[CustomQuestionTypeLoader] Failed to load "${formatName}":`, + errorMessage, + ); + result.errors.push({ format: formatName, error: errorMessage }); + } + } + + // Register all successfully loaded components + if (loadedComponents.size > 0) { + result.renderers = registerCustomQuestionTypes(loadedComponents); + console.log( + `[CustomQuestionTypeLoader] Registered ${loadedComponents.size} custom question type(s)`, + ); + } + + if (result.errors.length > 0) { + console.warn( + `[CustomQuestionTypeLoader] ${result.errors.length} type(s) failed to load:`, + result.errors.map((e) => e.format).join(', '), + ); + } + + return result; +} diff --git a/formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts b/formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts new file mode 100644 index 000000000..3e6cae1ea --- /dev/null +++ b/formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts @@ -0,0 +1,57 @@ +/** + * CustomQuestionTypeRegistry.ts + * + * Converts a map of { formatName → React component } into JSON Forms + * RendererRegistryEntries. Each entry gets an auto-generated tester that + * matches on the schema's `format` field. + * + * Usage: + * const renderers = registerCustomQuestionTypes(componentsMap); + * // renderers can then be spread into the JsonForms renderers array + */ + +import type { JsonFormsRendererRegistryEntry, RankedTester } from '@jsonforms/core'; +import { rankWith, schemaMatches } from '@jsonforms/core'; +import type { CustomQuestionTypeProps } from '../types/CustomQuestionTypeContract'; +import { createCustomQuestionTypeRenderer } from '../renderers/CustomQuestionTypeAdapter'; +import type React from 'react'; + +/** + * Creates a ranked tester for a custom question type based on its schema format. + * + * Uses priority 6 which is higher than default Material renderers (priority 3-5) + * but lower than specialized built-in question types (priority 10+). + */ +function createFormatTester(formatName: string): RankedTester { + return rankWith( + 6, + schemaMatches((schema) => { + return (schema as Record)?.format === formatName; + }), + ); +} + +/** + * Registers custom question types by creating JSON Forms renderer entries. + * + * @param components - Map of format name → React component + * @returns Array of JsonFormsRendererRegistryEntry ready to be used with + */ +export function registerCustomQuestionTypes( + components: Map>, +): JsonFormsRendererRegistryEntry[] { + const entries: JsonFormsRendererRegistryEntry[] = []; + + for (const [formatName, component] of components) { + const tester = createFormatTester(formatName); + const renderer = createCustomQuestionTypeRenderer(formatName, component); + + entries.push({ tester, renderer }); + + console.log( + `[CustomQuestionTypeRegistry] Registered renderer for format "${formatName}"`, + ); + } + + return entries; +} diff --git a/formulus-formplayer/src/types/CustomQuestionTypeContract.ts b/formulus-formplayer/src/types/CustomQuestionTypeContract.ts new file mode 100644 index 000000000..657d6e958 --- /dev/null +++ b/formulus-formplayer/src/types/CustomQuestionTypeContract.ts @@ -0,0 +1,67 @@ +/** + * CustomQuestionTypeContract.ts + * + * Defines the public interface that custom question type renderers must follow. + * Form authors create components that accept these props — no JSON Forms knowledge needed. + * + * Usage in JSON Schema: + * { "type": "number", "format": "x-rating-stars", "x-config": { "maxStars": 5 } } + * + * Usage in custom_app: + * custom_app/question_types/rating-stars/index.js + * export default function RatingStars({ value, config, onChange, validation }) { ... } + */ + +/** + * Props that every custom question type renderer receives. + */ +export interface CustomQuestionTypeProps { + /** Current field value (type depends on the question's JSON schema type) */ + value: unknown; + + /** + * Configuration from the schema's `x-config` property. + * For example, if schema has `"x-config": { "maxStars": 5 }`, + * then `config.maxStars === 5`. + */ + config: Record; + + /** Callback to update the field value. Call with the new value. */ + onChange: (newValue: unknown) => void; + + /** Current validation state for this field */ + validation: { + /** Whether the field currently has a validation error */ + error: boolean; + /** The validation error message (empty string if no error) */ + message: string; + }; + + /** Whether the field is currently enabled/editable */ + enabled: boolean; + + /** The field's unique path in the form data (e.g., "satisfaction") */ + fieldPath: string; + + /** Display label from the schema's `title` property */ + label: string; + + /** Optional description from the schema's `description` property */ + description?: string; +} + +/** + * Manifest passed from the native side describing available custom question types. + * Each entry maps a format string to the source code of the module that renders it. + * The RN side reads the JS file and passes the source string here for sandboxed evaluation. + */ +export interface CustomQuestionTypeManifest { + custom_types: Record< + string, + { + /** The JS source code of the module (read by RN via RNFS.readFile) */ + source: string; + } + >; +} + diff --git a/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts b/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts index fef86f436..e048cd8dc 100644 --- a/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts +++ b/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts @@ -46,6 +46,7 @@ export interface ExtensionMetadata { * @property {any} [formSchema] - JSON Schema for the form structure and validation (optional) * @property {any} [uiSchema] - UI Schema for form rendering layout (optional) * @property {ExtensionMetadata} [extensions] - Custom app extensions (optional) + * @property {object} [customQuestionTypes] - Custom question type manifest from custom_app (optional) */ export interface FormInitData { formType: string; @@ -56,6 +57,9 @@ export interface FormInitData { uiSchema?: unknown; operationId?: string; extensions?: ExtensionMetadata; + customQuestionTypes?: { + custom_types: Record; + }; } /** diff --git a/formulus/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx index 8dc1dc3e4..90a910005 100644 --- a/formulus/src/components/FormplayerModal.tsx +++ b/formulus/src/components/FormplayerModal.tsx @@ -35,6 +35,7 @@ import { databaseService } from '../database'; import { colors } from '../theme/colors'; import { FormSpec } from '../services'; // FormService will be imported directly import { ExtensionService } from '../services/ExtensionService'; +import { scanCustomQuestionTypes } from '../services/CustomQuestionTypeScanner'; import RNFS from 'react-native-fs'; import { useAppTheme } from '../contexts/AppThemeContext'; @@ -217,9 +218,9 @@ const FormplayerModal = forwardRef( }; // Load extensions for this form + const customAppPath = RNFS.DocumentDirectoryPath + '/app'; let extensions = undefined; try { - const customAppPath = RNFS.DocumentDirectoryPath + '/app'; const extensionService = ExtensionService.getInstance(); const mergedExtensions = await extensionService.getCustomAppExtensions( customAppPath, @@ -291,6 +292,25 @@ const FormplayerModal = forwardRef( return; } + // Scan custom question types (reads JS files, screens against blocklist) + let customQuestionTypes = undefined; + try { + const scanResult = await scanCustomQuestionTypes(customAppPath); + if (Object.keys(scanResult.custom_types).length > 0) { + customQuestionTypes = { + custom_types: scanResult.custom_types, + }; + } + if (scanResult.errors.length > 0) { + console.warn( + 'Some custom question types failed screening:', + scanResult.errors, + ); + } + } catch (error) { + console.warn('Failed to scan custom question types:', error); + } + const formInitData = { formType: formType.id, observationId: observationId, @@ -299,6 +319,7 @@ const FormplayerModal = forwardRef( formSchema: formType.schema, uiSchema: formType.uiSchema ?? {}, extensions, + customQuestionTypes, } as FormInitData; if (!webViewRef.current) { diff --git a/formulus/src/services/CustomQuestionTypeScanner.ts b/formulus/src/services/CustomQuestionTypeScanner.ts new file mode 100644 index 000000000..85db639ca --- /dev/null +++ b/formulus/src/services/CustomQuestionTypeScanner.ts @@ -0,0 +1,182 @@ +/** + * CustomQuestionTypeScanner.ts + * + * Scans the custom_app's `question_types/` directory on the device filesystem, + * reads each module's source code, and screens it against a blocklist of + * dangerous patterns before passing it to FormPlayer. + * + * This runs on the Formulus RN side (not in the WebView). + * + * Security: This is the first line of defense. Source code that contains + * dangerous API calls is rejected before it ever reaches the WebView. + */ + +import RNFS from 'react-native-fs'; + +export interface ScannedQuestionType { + /** The raw JS source code of the module */ + source: string; +} + +export interface ScanResult { + /** Successfully scanned custom types, keyed by format name (folder name) */ + custom_types: Record; + /** Errors encountered during scanning (types that were rejected or couldn't be read) */ + errors: Array<{ name: string; error: string }>; +} + +/** + * Patterns that indicate potentially dangerous code. + * If any of these are found in the source, the module is rejected. + */ +const BLOCKED_PATTERNS: Array<{ pattern: RegExp; description: string }> = [ + { pattern: /\bfetch\s*\(/, description: 'Network request via fetch()' }, + { + pattern: /\bXMLHttpRequest\b/, + description: 'Network request via XMLHttpRequest', + }, + { pattern: /\bWebSocket\b/, description: 'WebSocket connection' }, + { pattern: /\beval\s*\(/, description: 'Dynamic code evaluation via eval()' }, + { + pattern: /\bnew\s+Function\s*\(/, + description: 'Dynamic code evaluation via new Function()', + }, + { pattern: /\bdocument\.cookie\b/, description: 'Cookie access' }, + { pattern: /\blocalStorage\b/, description: 'localStorage access' }, + { pattern: /\bsessionStorage\b/, description: 'sessionStorage access' }, + { pattern: /\bindexedDB\b/, description: 'IndexedDB access' }, + { + pattern: /\bnavigator\.sendBeacon\b/, + description: 'Data exfiltration via sendBeacon', + }, + { + pattern: /\bimportScripts\s*\(/, + description: 'Script import via importScripts()', + }, +]; + +/** + * Screen source code against the blocklist. + * Returns null if the source is clean, or a description of the violation. + */ +function screenSource(source: string): string | null { + for (const { pattern, description } of BLOCKED_PATTERNS) { + if (pattern.test(source)) { + return description; + } + } + return null; +} + +/** + * Scan the `question_types/` directory inside the custom app path. + * + * For each subdirectory found: + * 1. Check for an `index.js` file + * 2. Read the file contents as a string + * 3. Screen the source against the blocklist + * 4. If clean, include in the result + * + * @param customAppPath - The root path of the custom app (e.g., RNFS.DocumentDirectoryPath + '/app') + * @returns Scanned question types and any errors + */ +export async function scanCustomQuestionTypes( + customAppPath: string, +): Promise { + const result: ScanResult = { + custom_types: {}, + errors: [], + }; + + const questionTypesDir = `${customAppPath}/question_types`; + + // Check if the question_types directory exists + const dirExists = await RNFS.exists(questionTypesDir); + if (!dirExists) { + console.log( + '[CustomQuestionTypeScanner] No question_types/ directory found at:', + questionTypesDir, + ); + return result; + } + + // Read all items in the question_types directory + let folders: RNFS.ReadDirItem[]; + try { + folders = await RNFS.readDir(questionTypesDir); + } catch (err) { + console.error( + '[CustomQuestionTypeScanner] Failed to read question_types directory:', + err, + ); + return result; + } + + // Process each subdirectory + for (const folder of folders) { + if (!folder.isDirectory()) { + continue; + } + + const formatName = folder.name; // e.g., "x-ranking" + const indexPath = `${folder.path}/index.js`; + + try { + // Check if index.js exists + const fileExists = await RNFS.exists(indexPath); + if (!fileExists) { + result.errors.push({ + name: formatName, + error: `No index.js found in question_types/${formatName}/`, + }); + continue; + } + + // Read the source code + const source = await RNFS.readFile(indexPath, 'utf8'); + + if (!source || source.trim().length === 0) { + result.errors.push({ + name: formatName, + error: 'index.js is empty', + }); + continue; + } + + // Screen against the blocklist + const violation = screenSource(source); + if (violation) { + result.errors.push({ + name: formatName, + error: `Blocked: ${violation}`, + }); + console.warn( + `[CustomQuestionTypeScanner] Rejected "${formatName}": ${violation}`, + ); + continue; + } + + // Source is clean — include it + result.custom_types[formatName] = { source }; + console.log( + `[CustomQuestionTypeScanner] Accepted "${formatName}" (${source.length} bytes)`, + ); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + result.errors.push({ + name: formatName, + error: `Failed to read: ${errorMessage}`, + }); + console.error( + `[CustomQuestionTypeScanner] Error processing "${formatName}":`, + errorMessage, + ); + } + } + + console.log( + `[CustomQuestionTypeScanner] Scan complete: ${Object.keys(result.custom_types).length} accepted, ${result.errors.length} errors`, + ); + + return result; +} diff --git a/formulus/src/webview/FormulusInterfaceDefinition.ts b/formulus/src/webview/FormulusInterfaceDefinition.ts index fef86f436..e048cd8dc 100644 --- a/formulus/src/webview/FormulusInterfaceDefinition.ts +++ b/formulus/src/webview/FormulusInterfaceDefinition.ts @@ -46,6 +46,7 @@ export interface ExtensionMetadata { * @property {any} [formSchema] - JSON Schema for the form structure and validation (optional) * @property {any} [uiSchema] - UI Schema for form rendering layout (optional) * @property {ExtensionMetadata} [extensions] - Custom app extensions (optional) + * @property {object} [customQuestionTypes] - Custom question type manifest from custom_app (optional) */ export interface FormInitData { formType: string; @@ -56,6 +57,9 @@ export interface FormInitData { uiSchema?: unknown; operationId?: string; extensions?: ExtensionMetadata; + customQuestionTypes?: { + custom_types: Record; + }; } /** From ea89e8f2cd30715033b7b1b652fdcc01b98556dc Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Thu, 19 Feb 2026 15:46:37 +0300 Subject: [PATCH 03/25] feat: enhance module resolution and improve custom question type handling --- formulus-formplayer/index.html | 1 + formulus-formplayer/src/App.tsx | 23 ++++- formulus-formplayer/src/index.tsx | 10 ++ .../renderers/CustomQuestionTypeAdapter.tsx | 97 +++++++++++++++++-- .../src/services/CustomQuestionTypeLoader.ts | 18 +++- .../services/CustomQuestionTypeRegistry.ts | 3 +- .../src/types/CustomQuestionTypeContract.ts | 11 ++- formulus-formplayer/tsconfig.json | 45 ++++----- formulus-formplayer/vite.config.ts | 3 +- .../main/assets/formplayer_dist/index.html | 2 + formulus/metro.config.js | 20 ++++ formulus/src/api/synkronus/index.ts | 14 ++- .../src/services/CustomQuestionTypeScanner.ts | 35 ++++--- formulus/src/services/FormService.ts | 5 +- packages/tokens/package.json | 9 ++ synkronus/pkg/appbundle/versioning.go | 5 +- 16 files changed, 237 insertions(+), 64 deletions(-) diff --git a/formulus-formplayer/index.html b/formulus-formplayer/index.html index 7d2bafd72..00b3b1095 100644 --- a/formulus-formplayer/index.html +++ b/formulus-formplayer/index.html @@ -10,6 +10,7 @@ Formulus Form Player + diff --git a/formulus-formplayer/src/App.tsx b/formulus-formplayer/src/App.tsx index 0de7ceec1..9e72ed61e 100644 --- a/formulus-formplayer/src/App.tsx +++ b/formulus-formplayer/src/App.tsx @@ -28,6 +28,7 @@ import { tokens } from './theme/tokens-adapter'; import Ajv from 'ajv'; import addErrors from 'ajv-errors'; import addFormats from 'ajv-formats'; +import * as MUI from '@mui/material'; // Import the FormulusInterface client import FormulusClient from './services/FormulusInterface'; @@ -230,7 +231,16 @@ export const customRenderers = [ numberStepperRenderer, ]; +// Expose React and MaterialUI to global scope for custom question type renderers +// This must be done synchronously at module load time so renderers can access them +if (typeof window !== 'undefined') { + (window as any).React = React; + (window as any).MaterialUI = MUI; + console.log('[App] Exposed React and MaterialUI to global scope for custom renderers'); +} + function App() { + // Initialize WebView mock ONLY in development mode and ONLY if ReactNativeWebView doesn't exist if ( process.env.NODE_ENV === 'development' && @@ -347,7 +357,7 @@ function App() { } // Start with built-in extensions (always available) - const allFunctions = getBuiltinExtensions(); + const allFunctions = getBuiltinExtensions() as Map any>; // Load extensions if provided if (extensions) { @@ -395,7 +405,7 @@ function App() { setCustomTypeRenderers(customQTResult.renderers); setCustomTypeFormats(customQTResult.formats); console.log( - `[Formplayer] Loaded ${customQTResult.renderers.length} custom question type(s)`, + `[Formplayer] Loaded ${customQTResult.renderers.length} custom question type(s): ${customQTResult.formats.join(', ')}`, ); if (customQTResult.errors.length > 0) { console.warn( @@ -832,12 +842,15 @@ function App() { }); // Register custom question type formats with AJV + // Custom question types use "format": "formatName" in schemas (not "type") + // This is required because JSON Schema only allows standard types in the "type" field if (customTypeFormats.length > 0) { - customTypeFormats.forEach((fmt) => { - instance.addFormat(fmt, () => true); + customTypeFormats.forEach((formatName) => { + // Register as format so AJV accepts "format": "formatName" in schemas + instance.addFormat(formatName, () => true); }); console.log( - `[Formplayer] Registered ${customTypeFormats.length} custom format(s) with AJV`, + `[Formplayer] Registered ${customTypeFormats.length} custom question type format(s) with AJV`, ); } diff --git a/formulus-formplayer/src/index.tsx b/formulus-formplayer/src/index.tsx index 30d12270c..38d4d719d 100644 --- a/formulus-formplayer/src/index.tsx +++ b/formulus-formplayer/src/index.tsx @@ -1,7 +1,17 @@ +import React from 'react'; import ReactDOM from 'react-dom/client'; +import * as MUI from '@mui/material'; import './index.css'; import App from './App'; +// Expose React and MaterialUI to global scope for custom question type renderers +// This MUST happen at the entry point before any other code runs +if (typeof window !== 'undefined') { + (window as any).React = React; + (window as any).MaterialUI = MUI; + console.log('[index] Exposed React and MaterialUI to global scope for custom renderers'); +} + const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement, ); diff --git a/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx b/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx index cf2a52fae..6c4b5c6aa 100644 --- a/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx +++ b/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx @@ -49,16 +49,47 @@ class CustomQuestionErrorBoundary extends Component< return (
- Custom question type "{this.props.formatName}" failed -
- {this.state.error?.message} + + ⚠️ Custom Question Type Error + +
+ The custom question type "{this.props.formatName}" encountered an error + and could not be rendered. +
+
+ + Error Details (click to expand) + +
+              {this.state.error?.message || 'Unknown error'}
+              {this.state.error?.stack && (
+                <>
+                  {'\n\n'}
+                  {this.state.error.stack}
+                
+              )}
+            
+
+
+ The form will continue to function, but this field cannot be edited. +
); } @@ -92,13 +123,63 @@ export function createCustomQuestionTypeRenderer( required, }) => { // Build the simplified props for the custom component + const hasErrors = errors && (Array.isArray(errors) ? errors.length > 0 : true); + const errorMessage = hasErrors + ? Array.isArray(errors) + ? errors.map((e: any) => e.message || String(e)).join(', ') + : String(errors) + : ''; + + // Extract all schema properties (except reserved ones) as config + // This allows parameters alongside "format" to be passed to the renderer + const schemaObj = schema as Record; + const RESERVED_PROPERTIES = new Set([ + 'type', + 'title', + 'description', + 'format', + 'enum', + 'const', + 'default', + 'required', + 'properties', + 'items', + 'oneOf', + 'anyOf', + 'allOf', + '$ref', + '$schema', + 'additionalProperties', + 'pattern', + 'minLength', + 'maxLength', + 'minimum', + 'maximum', + 'minItems', + 'maxItems', + ]); + + // Extract all non-reserved properties as config + const config: Record = {}; + for (const [key, value] of Object.entries(schemaObj)) { + if (!RESERVED_PROPERTIES.has(key) && !key.startsWith('$')) { + config[key] = value; + } + } + + // Merge with x-config (x-config takes precedence for explicit configuration) + const xConfig = schemaObj['x-config'] as Record | undefined; + if (xConfig) { + Object.assign(config, xConfig); + } + const customProps: CustomQuestionTypeProps = { value: data, - config: (schema as Record)?.['x-config'] as Record ?? {}, + config, onChange: (newValue: unknown) => handleChange(path, newValue), validation: { - error: Boolean(errors && errors.length > 0), - message: errors ?? '', + error: Boolean(hasErrors), + message: errorMessage, }, enabled: enabled ?? true, fieldPath: path, diff --git a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts index 54d3b82fe..55b87c619 100644 --- a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts +++ b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts @@ -16,6 +16,8 @@ * 3. Extracts and validates the default export (must be a React component function) * 4. Passes all loaded components to the registry * 5. Returns renderer entries + format strings for AJV registration + * + * Custom question types use "format": "formatName" in schemas (not "type"). */ import type { JsonFormsRendererRegistryEntry } from '@jsonforms/core'; @@ -53,13 +55,23 @@ function evaluateModuleInSandbox( const moduleObj = { exports }; // Get React from the global scope (it's available in the WebView) - const ReactLib = (window as unknown as Record).React; + // Try multiple ways to access it (window, globalThis, self) + const ReactLib = + (window as unknown as Record).React || + (globalThis as unknown as Record).React || + (self as unknown as Record).React; + if (!ReactLib) { + console.error('[CustomQuestionTypeLoader] React not found in window, globalThis, or self'); + console.error('[CustomQuestionTypeLoader] Available window keys:', Object.keys(window).slice(0, 20)); throw new Error('React is not available in the global scope'); } // Get MUI from the global scope (custom components may use Material UI) - const MUILib = (window as unknown as Record).MaterialUI; + const MUILib = + (window as unknown as Record).MaterialUI || + (globalThis as unknown as Record).MaterialUI || + (self as unknown as Record).MaterialUI; try { // Create a factory function with a restricted scope. @@ -154,7 +166,7 @@ export async function loadCustomQuestionTypes( if (result.errors.length > 0) { console.warn( - `[CustomQuestionTypeLoader] ${result.errors.length} type(s) failed to load:`, + `[CustomQuestionTypeLoader] ${result.errors.length} format(s) failed to load:`, result.errors.map((e) => e.format).join(', '), ); } diff --git a/formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts b/formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts index 3e6cae1ea..d7a99bd1e 100644 --- a/formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts +++ b/formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts @@ -26,7 +26,8 @@ function createFormatTester(formatName: string): RankedTester { return rankWith( 6, schemaMatches((schema) => { - return (schema as Record)?.format === formatName; + const schemaObj = schema as Record; + return schemaObj?.format === formatName; }), ); } diff --git a/formulus-formplayer/src/types/CustomQuestionTypeContract.ts b/formulus-formplayer/src/types/CustomQuestionTypeContract.ts index 657d6e958..955ba021f 100644 --- a/formulus-formplayer/src/types/CustomQuestionTypeContract.ts +++ b/formulus-formplayer/src/types/CustomQuestionTypeContract.ts @@ -5,10 +5,10 @@ * Form authors create components that accept these props — no JSON Forms knowledge needed. * * Usage in JSON Schema: - * { "type": "number", "format": "x-rating-stars", "x-config": { "maxStars": 5 } } + * { "type": "string", "format": "rating-stars", "x-config": { "maxStars": 5 } } * * Usage in custom_app: - * custom_app/question_types/rating-stars/index.js + * custom_app/question_types/rating-stars/renderer.js * export default function RatingStars({ value, config, onChange, validation }) { ... } */ @@ -20,9 +20,10 @@ export interface CustomQuestionTypeProps { value: unknown; /** - * Configuration from the schema's `x-config` property. - * For example, if schema has `"x-config": { "maxStars": 5 }`, - * then `config.maxStars === 5`. + * Configuration extracted from schema properties. + * Includes all properties alongside "format" (except reserved ones like type, title, etc.) + * and properties from "x-config" (x-config takes precedence). + * For example, if schema has `"format": "rating", "maxStars": 5`, then `config.maxStars === 5`. */ config: Record; diff --git a/formulus-formplayer/tsconfig.json b/formulus-formplayer/tsconfig.json index f3859f36f..3ae6deed4 100644 --- a/formulus-formplayer/tsconfig.json +++ b/formulus-formplayer/tsconfig.json @@ -1,22 +1,23 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "allowJs": true, - "checkJs": false, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" - }, - "include": ["src"] -} +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "allowJs": true, + "checkJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "exclude": ["src/**/__tests__/**", "src/**/*.test.ts"] +} diff --git a/formulus-formplayer/vite.config.ts b/formulus-formplayer/vite.config.ts index 785e5e251..5e9010355 100644 --- a/formulus-formplayer/vite.config.ts +++ b/formulus-formplayer/vite.config.ts @@ -38,7 +38,8 @@ export default defineConfig({ onwarn(warning, warn) { if ( warning.code === 'EVAL' || - (warning.message && warning.message.includes('ExtensionsLoader')) + (warning.message && warning.message.includes('ExtensionsLoader')) || + (warning.code === 'UNUSED_EXTERNAL_IMPORT' && warning.source?.includes('formulus-load.js')) ) { return; } diff --git a/formulus/android/app/src/main/assets/formplayer_dist/index.html b/formulus/android/app/src/main/assets/formplayer_dist/index.html index 309ee212a..f9b4e24f6 100644 --- a/formulus/android/app/src/main/assets/formplayer_dist/index.html +++ b/formulus/android/app/src/main/assets/formplayer_dist/index.html @@ -10,6 +10,7 @@ Formulus Form Player + @@ -18,5 +19,6 @@
+ diff --git a/formulus/metro.config.js b/formulus/metro.config.js index 1e6aae786..970f52a10 100644 --- a/formulus/metro.config.js +++ b/formulus/metro.config.js @@ -29,6 +29,18 @@ const extraModules = { projectRoot, 'node_modules/react-native-svg', ), + '@ode/components/react-native': path.resolve( + monorepoRoot, + 'packages/components/src/react-native/index.ts', + ), + '@ode/components/react-web': path.resolve( + monorepoRoot, + 'packages/components/src/react-web/index.ts', + ), + '@ode/tokens/dist/react-native/tokens-resolved': path.resolve( + monorepoRoot, + 'packages/tokens/dist/react-native/tokens-resolved.js', + ), }; /** @@ -44,12 +56,20 @@ const config = { unstable_enablePackageExports: true, extraNodeModules: extraModules, resolveRequest(context, moduleName, platform) { + // Handle forced modules (react, react-native) if (forcedModules[moduleName]) { return { type: 'sourceFile', filePath: path.join(forcedModules[moduleName], 'index.js'), }; } + // Handle @ode/components subpath exports + if (extraModules[moduleName]) { + return { + type: 'sourceFile', + filePath: extraModules[moduleName], + }; + } return context.resolveRequest(context, moduleName, platform); }, }, diff --git a/formulus/src/api/synkronus/index.ts b/formulus/src/api/synkronus/index.ts index 69d770803..17e7720a4 100644 --- a/formulus/src/api/synkronus/index.ts +++ b/formulus/src/api/synkronus/index.ts @@ -208,16 +208,22 @@ class SynkronusApi { progressCallback?.(80); // Atomic swap: remove old dirs, move staging content into place - if (await RNFS.exists(appDir)) await RNFS.unlink(appDir); - if (await RNFS.exists(formsDir)) await RNFS.unlink(formsDir); + if (await RNFS.exists(appDir)) { + await RNFS.unlink(appDir); + } + if (await RNFS.exists(formsDir)) { + await RNFS.unlink(formsDir); + } const stagingAppDir = `${tempExtractPath}/app`; const stagingFormsDir = `${tempExtractPath}/forms`; - if (await RNFS.exists(stagingAppDir)) + if (await RNFS.exists(stagingAppDir)) { await RNFS.moveFile(stagingAppDir, appDir); - if (await RNFS.exists(stagingFormsDir)) + } + if (await RNFS.exists(stagingFormsDir)) { await RNFS.moveFile(stagingFormsDir, formsDir); + } progressCallback?.(95); diff --git a/formulus/src/services/CustomQuestionTypeScanner.ts b/formulus/src/services/CustomQuestionTypeScanner.ts index 85db639ca..e1bc522d8 100644 --- a/formulus/src/services/CustomQuestionTypeScanner.ts +++ b/formulus/src/services/CustomQuestionTypeScanner.ts @@ -2,13 +2,20 @@ * CustomQuestionTypeScanner.ts * * Scans the custom_app's `question_types/` directory on the device filesystem, - * reads each module's source code, and screens it against a blocklist of - * dangerous patterns before passing it to FormPlayer. + * reads each module's source code (from renderer.js files), and screens it + * against a blocklist of dangerous patterns before passing it to FormPlayer. * * This runs on the Formulus RN side (not in the WebView). * * Security: This is the first line of defense. Source code that contains * dangerous API calls is rejected before it ever reaches the WebView. + * + * File structure: + * question_types/{formatName}/renderer.js + * + * Schema usage: + * { "type": "string", "format": "{formatName}", ... } + * The format name must match the directory name. */ import RNFS from 'react-native-fs'; @@ -19,7 +26,7 @@ export interface ScannedQuestionType { } export interface ScanResult { - /** Successfully scanned custom types, keyed by format name (folder name) */ + /** Successfully scanned custom question types, keyed by format name (folder name) */ custom_types: Record; /** Errors encountered during scanning (types that were rejected or couldn't be read) */ errors: Array<{ name: string; error: string }>; @@ -72,11 +79,14 @@ function screenSource(source: string): string | null { * Scan the `question_types/` directory inside the custom app path. * * For each subdirectory found: - * 1. Check for an `index.js` file + * 1. Check for a `renderer.js` file * 2. Read the file contents as a string * 3. Screen the source against the blocklist * 4. If clean, include in the result * + * The directory name becomes the format name used in schemas. + * Example: "ranking/" directory → use "format": "ranking" in schema + * * @param customAppPath - The root path of the custom app (e.g., RNFS.DocumentDirectoryPath + '/app') * @returns Scanned question types and any errors */ @@ -90,8 +100,9 @@ export async function scanCustomQuestionTypes( const questionTypesDir = `${customAppPath}/question_types`; - // Check if the question_types directory exists const dirExists = await RNFS.exists(questionTypesDir); + + // Check if the question_types directory exists if (!dirExists) { console.log( '[CustomQuestionTypeScanner] No question_types/ directory found at:', @@ -118,27 +129,27 @@ export async function scanCustomQuestionTypes( continue; } - const formatName = folder.name; // e.g., "x-ranking" - const indexPath = `${folder.path}/index.js`; + const formatName = folder.name; // e.g., "ranking" + const rendererPath = `${folder.path}/renderer.js`; try { - // Check if index.js exists - const fileExists = await RNFS.exists(indexPath); + // Check if renderer.js exists + const fileExists = await RNFS.exists(rendererPath); if (!fileExists) { result.errors.push({ name: formatName, - error: `No index.js found in question_types/${formatName}/`, + error: `No renderer.js found in question_types/${formatName}/`, }); continue; } // Read the source code - const source = await RNFS.readFile(indexPath, 'utf8'); + const source = await RNFS.readFile(rendererPath, 'utf8'); if (!source || source.trim().length === 0) { result.errors.push({ name: formatName, - error: 'index.js is empty', + error: 'renderer.js is empty', }); continue; } diff --git a/formulus/src/services/FormService.ts b/formulus/src/services/FormService.ts index 91c8fe0f4..917885684 100644 --- a/formulus/src/services/FormService.ts +++ b/formulus/src/services/FormService.ts @@ -115,12 +115,13 @@ export class FormService { } const formSpecFolders = await RNFS.readDir(formSpecsDir); - // Skip non-form directories (e.g. extensions/, .hidden) + // Skip non-form directories (e.g. extensions/, question_types/, .hidden) const formDirs = formSpecFolders.filter( f => f.isDirectory() && !f.name.startsWith('.') && - f.name !== 'extensions', + f.name !== 'extensions' && + f.name !== 'question_types', ); for (const formDir of formDirs) { diff --git a/packages/tokens/package.json b/packages/tokens/package.json index eff10608f..3f9bf7e3a 100644 --- a/packages/tokens/package.json +++ b/packages/tokens/package.json @@ -4,6 +4,15 @@ "description": "ODE Design System - Unified design tokens for React Native, React Web, and all ODE applications", "main": "dist/js/tokens.js", "types": "dist/js/tokens.d.ts", + "exports": { + ".": { + "react-native": "./dist/react-native/tokens-resolved.js", + "default": "./dist/js/tokens.js" + }, + "./dist/react-native/tokens-resolved": "./dist/react-native/tokens-resolved.js", + "./dist/react-native/tokens-resolved.js": "./dist/react-native/tokens-resolved.js", + "./dist/json/tokens.json": "./dist/json/tokens.json" + }, "files": [ "dist", "README.md" diff --git a/synkronus/pkg/appbundle/versioning.go b/synkronus/pkg/appbundle/versioning.go index 48279b4f7..d2399ea7b 100644 --- a/synkronus/pkg/appbundle/versioning.go +++ b/synkronus/pkg/appbundle/versioning.go @@ -39,7 +39,7 @@ func (s *Service) PushBundle(ctx context.Context, zipReader io.Reader) (*Manifes if err != nil { return nil, fmt.Errorf("failed to open zip file: %w", err) } - defer zipFile.Close() + // Note: We'll close zipFile explicitly before copying tempZipFile to bundle.zip // Validate the bundle structure if err := s.validateBundleStructure(&zipFile.Reader); err != nil { @@ -123,6 +123,9 @@ func (s *Service) PushBundle(ctx context.Context, zipReader io.Reader) (*Manifes dstFile.Close() } + // Close the zip reader before copying the temp file + zipFile.Close() + // Save the original zip to the version directory for direct download if _, err := tempZipFile.Seek(0, 0); err != nil { return nil, fmt.Errorf("failed to rewind zip for saving: %w", err) From be93a9122adde67981f9e098b8b97b0548a1b4ee Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Thu, 19 Feb 2026 15:50:13 +0300 Subject: [PATCH 04/25] added package changes --- packages/package-lock.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 packages/package-lock.json diff --git a/packages/package-lock.json b/packages/package-lock.json new file mode 100644 index 000000000..f20691ff2 --- /dev/null +++ b/packages/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "packages", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From 25a737e1e1c928e4921ade49aebd2cfcf695a735 Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Thu, 19 Feb 2026 16:19:40 +0300 Subject: [PATCH 05/25] chore: apply prettier formatting fixes --- formulus-formplayer/src/App.tsx | 20 +++++++++----- formulus-formplayer/src/index.tsx | 4 ++- .../renderers/CustomQuestionTypeAdapter.tsx | 27 +++++++++++-------- .../src/services/CustomQuestionTypeLoader.ts | 25 ++++++++++++----- .../services/CustomQuestionTypeRegistry.ts | 7 +++-- .../src/types/CustomQuestionTypeContract.ts | 1 - 6 files changed, 56 insertions(+), 28 deletions(-) diff --git a/formulus-formplayer/src/App.tsx b/formulus-formplayer/src/App.tsx index 9e72ed61e..6d336efa9 100644 --- a/formulus-formplayer/src/App.tsx +++ b/formulus-formplayer/src/App.tsx @@ -236,11 +236,12 @@ export const customRenderers = [ if (typeof window !== 'undefined') { (window as any).React = React; (window as any).MaterialUI = MUI; - console.log('[App] Exposed React and MaterialUI to global scope for custom renderers'); + console.log( + '[App] Exposed React and MaterialUI to global scope for custom renderers', + ); } function App() { - // Initialize WebView mock ONLY in development mode and ONLY if ReactNativeWebView doesn't exist if ( process.env.NODE_ENV === 'development' && @@ -357,7 +358,10 @@ function App() { } // Start with built-in extensions (always available) - const allFunctions = getBuiltinExtensions() as Map any>; + const allFunctions = getBuiltinExtensions() as Map< + string, + (...args: any[]) => any + >; // Load extensions if provided if (extensions) { @@ -401,7 +405,8 @@ function App() { const customQTManifest = initData.customQuestionTypes; if (customQTManifest) { try { - const customQTResult = await loadCustomQuestionTypes(customQTManifest); + const customQTResult = + await loadCustomQuestionTypes(customQTManifest); setCustomTypeRenderers(customQTResult.renderers); setCustomTypeFormats(customQTResult.formats); console.log( @@ -414,7 +419,10 @@ function App() { ); } } catch (error) { - console.error('[Formplayer] Failed to load custom question types:', error); + console.error( + '[Formplayer] Failed to load custom question types:', + error, + ); setCustomTypeRenderers([]); setCustomTypeFormats([]); } @@ -845,7 +853,7 @@ function App() { // Custom question types use "format": "formatName" in schemas (not "type") // This is required because JSON Schema only allows standard types in the "type" field if (customTypeFormats.length > 0) { - customTypeFormats.forEach((formatName) => { + customTypeFormats.forEach(formatName => { // Register as format so AJV accepts "format": "formatName" in schemas instance.addFormat(formatName, () => true); }); diff --git a/formulus-formplayer/src/index.tsx b/formulus-formplayer/src/index.tsx index 38d4d719d..f64075fec 100644 --- a/formulus-formplayer/src/index.tsx +++ b/formulus-formplayer/src/index.tsx @@ -9,7 +9,9 @@ import App from './App'; if (typeof window !== 'undefined') { (window as any).React = React; (window as any).MaterialUI = MUI; - console.log('[index] Exposed React and MaterialUI to global scope for custom renderers'); + console.log( + '[index] Exposed React and MaterialUI to global scope for custom renderers', + ); } const root = ReactDOM.createRoot( diff --git a/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx b/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx index 6c4b5c6aa..898c04576 100644 --- a/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx +++ b/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx @@ -55,14 +55,13 @@ class CustomQuestionErrorBoundary extends Component< backgroundColor: '#ffebee', color: '#c62828', margin: '8px 0', - }} - > + }}> ⚠️ Custom Question Type Error
- The custom question type "{this.props.formatName}" encountered an error - and could not be rendered. + The custom question type "{this.props.formatName}"{' '} + encountered an error and could not be rendered.
@@ -76,8 +75,7 @@ class CustomQuestionErrorBoundary extends Component< borderRadius: '4px', overflow: 'auto', fontSize: '0.8em', - }} - > + }}> {this.state.error?.message || 'Unknown error'} {this.state.error?.stack && ( <> @@ -87,7 +85,12 @@ class CustomQuestionErrorBoundary extends Component< )}
-
+
The form will continue to function, but this field cannot be edited.
@@ -123,7 +126,8 @@ export function createCustomQuestionTypeRenderer( required, }) => { // Build the simplified props for the custom component - const hasErrors = errors && (Array.isArray(errors) ? errors.length > 0 : true); + const hasErrors = + errors && (Array.isArray(errors) ? errors.length > 0 : true); const errorMessage = hasErrors ? Array.isArray(errors) ? errors.map((e: any) => e.message || String(e)).join(', ') @@ -168,7 +172,9 @@ export function createCustomQuestionTypeRenderer( } // Merge with x-config (x-config takes precedence for explicit configuration) - const xConfig = schemaObj['x-config'] as Record | undefined; + const xConfig = schemaObj['x-config'] as + | Record + | undefined; if (xConfig) { Object.assign(config, xConfig); } @@ -192,8 +198,7 @@ export function createCustomQuestionTypeRenderer( title={label} description={description} required={required} - error={errors} - > + error={errors}> diff --git a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts index 55b87c619..4262b5f2e 100644 --- a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts +++ b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts @@ -60,10 +60,15 @@ function evaluateModuleInSandbox( (window as unknown as Record).React || (globalThis as unknown as Record).React || (self as unknown as Record).React; - + if (!ReactLib) { - console.error('[CustomQuestionTypeLoader] React not found in window, globalThis, or self'); - console.error('[CustomQuestionTypeLoader] Available window keys:', Object.keys(window).slice(0, 20)); + console.error( + '[CustomQuestionTypeLoader] React not found in window, globalThis, or self', + ); + console.error( + '[CustomQuestionTypeLoader] Available window keys:', + Object.keys(window).slice(0, 20), + ); throw new Error('React is not available in the global scope'); } @@ -93,7 +98,8 @@ function evaluateModuleInSandbox( } // Extract the component from exports (support both default and module.exports patterns) - const component = (moduleObj.exports as Record).default ?? moduleObj.exports; + const component = + (moduleObj.exports as Record).default ?? moduleObj.exports; if (typeof component !== 'function') { throw new Error( @@ -121,8 +127,13 @@ export async function loadCustomQuestionTypes( errors: [], }; - if (!manifest?.custom_types || Object.keys(manifest.custom_types).length === 0) { - console.log('[CustomQuestionTypeLoader] No custom question types in manifest'); + if ( + !manifest?.custom_types || + Object.keys(manifest.custom_types).length === 0 + ) { + console.log( + '[CustomQuestionTypeLoader] No custom question types in manifest', + ); return result; } @@ -167,7 +178,7 @@ export async function loadCustomQuestionTypes( if (result.errors.length > 0) { console.warn( `[CustomQuestionTypeLoader] ${result.errors.length} format(s) failed to load:`, - result.errors.map((e) => e.format).join(', '), + result.errors.map(e => e.format).join(', '), ); } diff --git a/formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts b/formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts index d7a99bd1e..23350abc8 100644 --- a/formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts +++ b/formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts @@ -10,7 +10,10 @@ * // renderers can then be spread into the JsonForms renderers array */ -import type { JsonFormsRendererRegistryEntry, RankedTester } from '@jsonforms/core'; +import type { + JsonFormsRendererRegistryEntry, + RankedTester, +} from '@jsonforms/core'; import { rankWith, schemaMatches } from '@jsonforms/core'; import type { CustomQuestionTypeProps } from '../types/CustomQuestionTypeContract'; import { createCustomQuestionTypeRenderer } from '../renderers/CustomQuestionTypeAdapter'; @@ -25,7 +28,7 @@ import type React from 'react'; function createFormatTester(formatName: string): RankedTester { return rankWith( 6, - schemaMatches((schema) => { + schemaMatches(schema => { const schemaObj = schema as Record; return schemaObj?.format === formatName; }), diff --git a/formulus-formplayer/src/types/CustomQuestionTypeContract.ts b/formulus-formplayer/src/types/CustomQuestionTypeContract.ts index 955ba021f..7a46ca806 100644 --- a/formulus-formplayer/src/types/CustomQuestionTypeContract.ts +++ b/formulus-formplayer/src/types/CustomQuestionTypeContract.ts @@ -65,4 +65,3 @@ export interface CustomQuestionTypeManifest { } >; } - From a77df63ab3e32cc9e153624e72ff5ed9b2b15881 Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Thu, 19 Feb 2026 17:08:03 +0300 Subject: [PATCH 06/25] Update md file --- .../custom-question-types-architecture.md | 328 ++++++++++++++---- 1 file changed, 263 insertions(+), 65 deletions(-) diff --git a/formulus-formplayer/docs/custom-question-types-architecture.md b/formulus-formplayer/docs/custom-question-types-architecture.md index bb53d1cab..1fb24a8fd 100644 --- a/formulus-formplayer/docs/custom-question-types-architecture.md +++ b/formulus-formplayer/docs/custom-question-types-architecture.md @@ -32,14 +32,21 @@ formulus/src/ (FORMULUS — runs in React Native) ``` custom_app/ └── question_types/ - ├── x-ranking/ - │ └── index.js ← default export: React component - ├── x-dynamicEnum/ - │ └── index.js - └── x-custom-text/ - └── index.js + ├── ranking/ + │ └── renderer.js ← default export: React component + ├── select-person/ + │ └── renderer.js + └── test-simple/ + └── renderer.js ``` +**Note:** The folder name becomes the format string. For example: +- `question_types/ranking/renderer.js` → `format: "ranking"` +- `question_types/select-person/renderer.js` → `format: "select-person"` +- `question_types/test-simple/renderer.js` → `format: "test-simple"` + +**Important:** The scanner specifically looks for `renderer.js` (not `index.js`). The file must be named `renderer.js` in each question type directory. + --- ## Security Model — Source Extraction @@ -79,10 +86,22 @@ const factory = new Function( sourceString // ← sent from RN as a string, not a file path ); +// React and MaterialUI are accessed from global scope +const ReactLib = window.React || globalThis.React || self.React; +const MUILib = window.MaterialUI || globalThis.MaterialUI || self.MaterialUI; + +factory(moduleObj, exports, ReactLib, MUILib); + // Custom code CAN access: React, MaterialUI, module, exports // Custom code CANNOT access: fetch, document, localStorage, window, etc. ``` +**Important Implementation Details:** +- React and MaterialUI are injected into the global scope by the WebView before custom question types are loaded +- The sandbox factory function receives these as explicit parameters, ensuring they're the only globals accessible +- If React or MaterialUI are not found in the global scope, loading fails with a clear error message +- The code supports both CommonJS patterns: `module.exports = Component` and `module.exports.default = Component` + --- ## How Module Loading Works @@ -99,14 +118,14 @@ When the custom_app archive is unzipped, files land on the device filesystem: │ ├── p_focal/schema.json │ └── ... └── question_types/ - ├── x-ranking/index.js ← pairwise Elo ranking UI - ├── x-dynamicEnum/index.js ← dynamic choice list from DB queries - └── x-custom-text/index.js ← enhanced text input + ├── ranking/renderer.js ← pairwise Elo ranking UI + ├── select-person/renderer.js ← person selection with search + └── test-simple/renderer.js ← simple test renderer ``` ### 2. Formulus RN Scans, Reads & Screens -`CustomQuestionTypeScanner.ts` scans `question_types/`, reads each `index.js` as a raw string, +`CustomQuestionTypeScanner.ts` scans `question_types/`, reads each `renderer.js` as a raw string, and screens it against the blocklist: ```typescript @@ -116,7 +135,7 @@ const folders = await RNFS.readDir(questionTypesDir); for (const folder of folders) { if (folder.isDirectory()) { - const source = await RNFS.readFile(`${folder.path}/index.js`, 'utf8'); + const source = await RNFS.readFile(`${folder.path}/renderer.js`, 'utf8'); // Screen against blocklist const violation = screenSource(source); @@ -136,19 +155,21 @@ for (const folder of folders) { ```json { "custom_types": { - "x-ranking": { - "source": "(function() { 'use strict'; ... module.exports = RankingRenderer; })()" + "ranking": { + "source": "(function() { 'use strict'; ... module.exports = { default: RankingRenderer }; })()" }, - "x-dynamicEnum": { - "source": "(function() { 'use strict'; ... module.exports = DynamicEnumControl; })()" + "select-person": { + "source": "(function() { 'use strict'; ... module.exports = { default: SelectPersonRenderer }; })()" }, - "x-custom-text": { - "source": "(function() { 'use strict'; ... module.exports = CustomTextRenderer; })()" + "test-simple": { + "source": "(function() { 'use strict'; ... module.exports = { default: TestSimpleRenderer }; })()" } } } ``` +**Note:** The format name matches the folder name in `question_types/`. The scanner reads the file content and includes it as a source string in the manifest. + ### 3. FormInitData Carries the Source Strings In `FormplayerModal.tsx`, `initializeForm()` calls the scanner and includes the result: @@ -183,26 +204,57 @@ the WebView. Then `CustomQuestionTypeLoader.ts` evaluates each source in a scope ```typescript // CustomQuestionTypeLoader.ts — evaluateModuleInSandbox() -const exports = {}; +const exports: Record = {}; const moduleObj = { exports }; +// Access React and MaterialUI from global scope +const ReactLib = window.React || globalThis.React || self.React; +const MUILib = window.MaterialUI || globalThis.MaterialUI || self.MaterialUI; + +if (!ReactLib) { + throw new Error('React is not available in the global scope'); +} + +// Create factory with restricted scope const factory = new Function( 'module', 'exports', 'React', 'MaterialUI', - meta.source, + sourceString ); -factory(moduleObj, exports, React, MaterialUI); +factory(moduleObj, exports, ReactLib, MUILib); -// Extract only the component +// Extract the component (supports both default and named exports) const component = moduleObj.exports.default ?? moduleObj.exports; + +// Validate it's a function +if (typeof component !== 'function') { + throw new Error(`Module does not export a valid React component`); +} ``` +**Error Handling:** +- Each module evaluation is wrapped in try-catch +- Failed modules are logged with detailed error messages +- Loading continues for other modules even if one fails +- Errors are collected and returned in the result for debugging + ### 5. Registry & Rendering `CustomQuestionTypeRegistry.ts` takes each loaded component and: - Auto-generates a tester: `rankWith(6, schemaMatches(s => s.format === name))` + - Priority 6 is higher than default Material renderers (3-5) but lower than specialized built-ins (10+) - Creates a renderer entry via `CustomQuestionTypeAdapter.tsx` -- Registers the format with AJV: `ajv.addFormat('x-ranking', () => true)` +- Returns renderer entries and format strings for AJV registration + +**Renderer Creation:** +- Each custom question type is wrapped in `QuestionShell` for consistent styling +- An `ErrorBoundary` catches any crashes in custom components +- The adapter maps JSON Forms `ControlProps` to simplified `CustomQuestionTypeProps` +- Config is extracted from schema properties (excluding reserved ones) and merged with `x-config` + +**AJV Format Registration:** +- Format strings are registered with AJV to prevent validation errors for unknown formats +- Registration happens in `App.tsx` after loading: `ajv.addFormat(formatName, () => true)` --- @@ -212,9 +264,9 @@ const component = moduleObj.exports.default ?? moduleObj.exports; ┌─────────────────────────────────────────────────────────────┐ │ DEVICE STORAGE (after custom_app unzip) │ │ │ -│ /Documents/app/question_types/x-ranking/index.js │ -│ /Documents/app/question_types/x-dynamicEnum/index.js │ -│ /Documents/app/question_types/x-custom-text/index.js │ +│ /Documents/app/question_types/ranking/renderer.js │ +│ /Documents/app/question_types/select-person/renderer.js │ +│ /Documents/app/question_types/test-simple/renderer.js │ └────────────────────────┬────────────────────────────────────┘ │ RNFS.readFile() → string ▼ @@ -224,7 +276,7 @@ const component = moduleObj.exports.default ?? moduleObj.exports; │ 1. Reads each index.js as a raw string │ │ 2. Screens against blocklist (fetch, eval, etc.) │ │ 3. Builds manifest with source strings: │ -│ { "x-ranking": { source: "..." } } │ +│ { "ranking": { source: "..." } } │ │ 4. Rejected modules → logged as warnings │ └────────────────────────┬────────────────────────────────────┘ │ FormInitData.customQuestionTypes @@ -242,10 +294,13 @@ const component = moduleObj.exports.default ?? moduleObj.exports; │ CustomQuestionTypeLoader.ts — SANDBOX │ │ │ │ For each entry in manifest.custom_types: │ -│ new Function('module','exports','React','MaterialUI', │ -│ source) │ -│ Extracts module.exports.default (React component) │ -│ Validates it's a function │ +│ 1. Access React/MaterialUI from global scope │ +│ 2. new Function('module','exports','React','MaterialUI',│ +│ source) │ +│ 3. Execute factory(moduleObj, exports, React, MUI) │ +│ 4. Extract module.exports.default or module.exports │ +│ 5. Validate it's a function │ +│ 6. Collect errors if evaluation fails │ │ ❌ No access to: fetch, document, localStorage, etc. │ └────────────────────────┬────────────────────────────────────┘ │ @@ -257,6 +312,7 @@ const component = moduleObj.exports.default ?? moduleObj.exports; │ Auto-generates a tester: │ │ rankWith(6, schemaMatches(s => s.format === name)) │ │ Creates renderer entry via adapter │ +│ Returns: { renderers[], formats[] } │ └────────────────────────┬────────────────────────────────────┘ │ ┌──────────┴──────────┐ @@ -265,8 +321,9 @@ const component = moduleObj.exports.default ?? moduleObj.exports; │ AJV Registration │ │ JsonForms Renderers Array │ │ │ │ │ │ ajv.addFormat( │ │ [ │ -│ 'x-ranking', │ │ ...builtInRenderers, │ -│ () => true │ │ ...customTypeRenderers, ← NEW │ +│ 'ranking', │ │ ...builtInRenderers, │ +│ 'select-person', │ │ ...customTypeRenderers, ← NEW │ +│ () => true │ │ ] │ │ ) │ │ ] │ │ │ │ │ │ Prevents AJV from │ │ Testers run top-to-bottom, │ @@ -282,13 +339,20 @@ const component = moduleObj.exports.default ?? moduleObj.exports; │ ───────────────────── ──────────────────────── │ │ data value │ │ handleChange(path, val) onChange(val) │ -│ errors (string) validation { error, msg } │ -│ schema['x-config'] config │ +│ errors (string/array) validation { error, msg } │ +│ schema (all props) config (merged with x-config) │ │ enabled enabled │ │ path fieldPath │ │ label, description label, description │ │ │ +│ Config extraction: │ +│ - All schema properties (except reserved) → config │ +│ - x-config properties override schema properties │ +│ - Reserved: type, title, description, format, enum, etc.│ +│ │ │ Wraps in: QuestionShell + ErrorBoundary │ +│ - ErrorBoundary shows user-friendly error UI │ +│ - Form continues to function if component crashes │ └────────────────────────┬────────────────────────────────────┘ │ ▼ @@ -307,69 +371,200 @@ const component = moduleObj.exports.default ?? moduleObj.exports; ### Ranking Question -Used in `p_focal` — pairwise Elo ranking of people by social attributes: +Used for pairwise comparison ranking of people (similar to ODK-X OMO style): ```json { - "ranking_result": { - "type": "object", - "format": "x-ranking", - "x-config": { - "sexFilter": "female", - "hardLimit": 250 - } + "ranking_field": { + "type": "array", + "format": "ranking", + "title": "Rank People", + "description": "Rank the people in order of preference", + "items": { + "type": "string" + }, + "people": [ + { + "id": "person1", + "name": "John Doe", + "age": 35, + "clan": "Alpha", + "sex": "male", + "photo_uriFragment": null + }, + { + "id": "person2", + "name": "Jane Smith", + "age": 28, + "clan": "Beta", + "sex": "female", + "photo_uriFragment": null + } + ], + "promptText": "Select the person you prefer" } } ``` -### Dynamic Enum Question +**Key points:** +- `format: "ranking"` (no "x-" prefix needed) +- `people` array is passed directly in schema (becomes `config.people`) +- `promptText` is optional and becomes `config.promptText` +- Value stored is an array of person IDs in ranked order +- Uses Elo-style pairwise comparison algorithm + +### Select Person Question -Used across many forms — dropdown choices populated from database queries: +Used for selecting a person from a list with optional search: ```json { - "selected_person": { + "select_person_field": { "type": "string", - "format": "x-dynamicEnum", - "x-config": { - "query": "p_consent", - "params": { - "scope": "{{data.scope}}" + "format": "select-person", + "title": "Select Person", + "description": "Choose a person from the list", + "showSearch": true, + "showPhoto": false, + "people": [ + { + "id": "person1", + "name": "John Doe", + "age": 35, + "clan": "Alpha", + "sex": "male" }, - "valueField": "observationId", - "labelField": "data.name" - } + { + "id": "person2", + "name": "Jane Smith", + "age": 28, + "clan": "Beta", + "sex": "female" + } + ] } } ``` -### Custom Text Question +**Key points:** +- `format: "select-person"` (no "x-" prefix needed) +- `people` array is passed directly in schema (becomes `config.people`) +- `showSearch` (default: true) enables searchable autocomplete +- `showPhoto` (default: false) shows person photos if available +- Value stored is the selected person's ID (string) + +### Simple Test Question -Enhanced text input with configurable multiline and placeholder: +Minimal example for testing the custom question type system: ```json { - "notes": { + "test_custom_field": { "type": "string", - "format": "x-custom-text", - "maxLength": 500, + "format": "test-simple", + "title": "Test Custom Question Type", + "description": "This field uses the test-simple custom question type renderer", + "placeholder": "Enter test value here...", + "maxLength": 50 + } +} +``` + +**Key points:** +- `format: "test-simple"` (no "x-" prefix needed) +- `placeholder` is passed directly in schema (becomes `config.placeholder`) +- Standard JSON Schema validation (`maxLength`) still applies + +### Using x-config (Alternative) + +You can also use `x-config` for explicit configuration that overrides schema properties: + +```json +{ + "custom_field": { + "type": "string", + "format": "my-custom-type", + "title": "My Field", + "maxLength": 100, "x-config": { - "placeholder": "Enter field notes...", - "helperText": "Describe any notable observations" + "customParam": "value", + "maxLength": 200 } } } ``` +In this case, `config.maxLength` will be `200` (from `x-config`), not `100` (from schema property). + **What happens for each:** -1. `format: "x-ranking"` → tester matches → the ranking renderer is used -2. `x-config` → passed as `props.config` to the author's component -3. Standard JSON Schema keywords (`type`, `maxLength`, etc.) → validated by AJV as normal -4. AJV doesn't reject the custom format strings because we registered them +1. `format: "ranking"` → tester matches → the ranking renderer is used +2. Schema properties (except reserved ones) → passed as `props.config` to the author's component +3. `x-config` properties → override schema properties in config +4. Standard JSON Schema keywords (`type`, `maxLength`, etc.) → validated by AJV as normal +5. AJV doesn't reject the custom format strings because we registered them --- +## Implementation Details + +### Security Model Implementation + +**Source Extraction Flow:** +1. RN side (`CustomQuestionTypeScanner`) reads JS files as raw strings +2. Static blocklist screening rejects dangerous patterns (fetch, eval, localStorage, etc.) +3. Clean source strings are passed in `FormInitData.customQuestionTypes` +4. WebView side (`CustomQuestionTypeLoader`) evaluates source in scoped sandbox +5. Only React, MaterialUI, module, and exports are accessible to custom code + +**Global Scope Access:** +- React and MaterialUI must be available in the WebView's global scope before loading +- The loader checks `window`, `globalThis`, and `self` for these libraries +- If not found, loading fails with a clear error message +- This ensures custom code can use React hooks and Material UI components + +**Error Handling:** +- Each module evaluation is wrapped in try-catch +- Failed modules are logged but don't stop other modules from loading +- Errors are collected and returned: `{ format: string, error: string }[]` +- The registry only processes successfully loaded components + +### Module Export Patterns + +Custom question type modules can export components in multiple ways: +```javascript +// Pattern 1: Object with default property (recommended) +module.exports = { + default: function MyComponent(props) { ... } +}; + +// Pattern 2: Direct default export +module.exports.default = function MyComponent(props) { ... }; + +// Pattern 3: Direct export (also supported) +module.exports = function MyComponent(props) { ... }; + +// Pattern 4: Named export (also supported) +module.exports.MyComponent = function MyComponent(props) { ... }; +``` + +The loader checks `module.exports.default` first, then falls back to `module.exports`. The recommended pattern is Pattern 1 (object with default property) as used in the AnthroCollect examples. + +### Config Extraction + +The adapter extracts configuration from the schema: +- All schema properties except reserved ones become `config` +- Reserved properties: `type`, `title`, `description`, `format`, `enum`, `const`, `default`, `required`, `properties`, `items`, `oneOf`, `anyOf`, `allOf`, `$ref`, `$schema`, validation keywords, etc. +- Properties from `x-config` override schema properties +- This allows passing parameters like `maxStars: 5` directly in the schema + +### Tester Priority + +Custom question type testers use priority 6: +- Higher than default Material renderers (priority 3-5) +- Lower than specialized built-in question types (priority 10+) +- Ensures custom types are selected when format matches, but built-ins take precedence for their specific formats + ## Implementation Plan (completed) All changes below have been implemented. @@ -388,7 +583,10 @@ All changes below have been implemented. |------|--------| | `FormulusInterfaceDefinition.ts` | `modulePath` → `source` (mirror) | | `CustomQuestionTypeContract.ts` | `modulePath` → `source` in `CustomQuestionTypeManifest` | -| `CustomQuestionTypeLoader.ts` | Rewritten: `import()` → `new Function()` sandbox | +| `CustomQuestionTypeLoader.ts` | Rewritten: `import()` → `new Function()` sandbox with React/MUI from global scope | +| `CustomQuestionTypeRegistry.ts` | Auto-generates testers with priority 6, creates renderer entries | +| `CustomQuestionTypeAdapter.tsx` | Maps ControlProps → CustomQuestionTypeProps, wraps in ErrorBoundary | +| `App.tsx` | Orchestrates loading, registers formats with AJV, merges with built-in renderers | ### Key Files — Full Reference From 265116acdd772c5e0e8d90d99004f43731802c1b Mon Sep 17 00:00:00 2001 From: Najuna Date: Fri, 20 Feb 2026 13:54:06 +0300 Subject: [PATCH 07/25] fix: resolve @ode/tokens import path for Dashboard build --- packages/tokens/package.json | 2 ++ synkronus-portal/src/pages/Dashboard.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/tokens/package.json b/packages/tokens/package.json index 3f9bf7e3a..2f41597b6 100644 --- a/packages/tokens/package.json +++ b/packages/tokens/package.json @@ -11,6 +11,8 @@ }, "./dist/react-native/tokens-resolved": "./dist/react-native/tokens-resolved.js", "./dist/react-native/tokens-resolved.js": "./dist/react-native/tokens-resolved.js", + "./dist/js/tokens": "./dist/js/tokens.js", + "./dist/css/tokens.css": "./dist/css/tokens.css", "./dist/json/tokens.json": "./dist/json/tokens.json" }, "files": [ diff --git a/synkronus-portal/src/pages/Dashboard.tsx b/synkronus-portal/src/pages/Dashboard.tsx index 0ad736d9f..020a54f69 100644 --- a/synkronus-portal/src/pages/Dashboard.tsx +++ b/synkronus-portal/src/pages/Dashboard.tsx @@ -43,7 +43,7 @@ import { HiChevronDown, HiCircleStack, } from 'react-icons/hi2'; -import { ColorBrandPrimary500 } from '@ode/tokens/dist/js/tokens'; +import { ColorBrandPrimary500 } from '@ode/tokens'; import odeLogo from '../assets/ode_logo.png'; import dashboardBackgroundDark from '../assets/dashboard-background.png'; import dashboardBackgroundLight from '../assets/dashboard-background-light.png'; From 07c998839da01248eb4dd06086c89753ec44a002 Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Thu, 26 Feb 2026 10:46:07 +0300 Subject: [PATCH 08/25] feat: clean up - Added a private method to recursively remove directories and their contents, handling nested directories and permission issues. - Updated the app bundle extraction process to use a unique staging path, improving conflict management. - Enhanced error handling for directory creation and extraction, ensuring non-fatal errors are logged without interrupting the process. - Increased the timeout for form initialization to reduce false positives in loading status. --- formulus-formplayer/src/App.tsx | 53 ++++++---- formulus-formplayer/src/index.tsx | 9 +- formulus/src/api/synkronus/index.ts | 150 +++++++++++++++++++++++++--- synkronus/cmd/genhash/main.go | 5 + 4 files changed, 183 insertions(+), 34 deletions(-) diff --git a/formulus-formplayer/src/App.tsx b/formulus-formplayer/src/App.tsx index bc942f5db..fa28cdbf3 100644 --- a/formulus-formplayer/src/App.tsx +++ b/formulus-formplayer/src/App.tsx @@ -651,29 +651,46 @@ function App() { } // Timeout logic: if onFormInit is not called by native side + // Note: This timeout often fires as a false positive when the form is actually loading successfully. + // We use a longer timeout (20s) and only show error after additional delay to reduce false positives. const initTimeout = setTimeout(() => { if (isLoadingRef.current) { - // Check ref to see if still loading - console.warn('onFormInit was not called within timeout period (10s).'); - setLoadError( - 'Failed to initialize form: No data received from native host. Please try again.', - ); - setIsLoading(false); - isLoadingRef.current = false; - if ( - window.ReactNativeWebView && - window.ReactNativeWebView.postMessage - ) { - window.ReactNativeWebView.postMessage( - JSON.stringify({ - type: 'error', - message: - 'Initialization timeout in WebView: onFormInit not called.', - }), + // Only log a debug message - don't show warning to user yet + // The form may still be loading successfully + if (process.env.NODE_ENV === 'development') { + console.debug( + '[Formplayer] onFormInit not yet received (timeout: 20s). Still waiting...', ); } + // Only show error if we're still loading after an additional delay + // This prevents false positives when form loads successfully but slightly delayed + setTimeout(() => { + if (isLoadingRef.current) { + // Only now show error - form truly failed to load + console.warn( + '[Formplayer] onFormInit timeout: Form failed to initialize after extended wait.', + ); + setLoadError( + 'Failed to initialize form: No data received from native host. Please try again.', + ); + setIsLoading(false); + isLoadingRef.current = false; + if ( + window.ReactNativeWebView && + window.ReactNativeWebView.postMessage + ) { + window.ReactNativeWebView.postMessage( + JSON.stringify({ + type: 'error', + message: + 'Initialization timeout in WebView: onFormInit not called.', + }), + ); + } + } + }, 5000); // Additional 5 seconds before showing actual error } - }, 10000); // 10 second timeout + }, 20000); // Increased to 20 seconds to reduce false positives // Cleanup function when component unmounts return () => { diff --git a/formulus-formplayer/src/index.tsx b/formulus-formplayer/src/index.tsx index f64075fec..fa36a8731 100644 --- a/formulus-formplayer/src/index.tsx +++ b/formulus-formplayer/src/index.tsx @@ -9,9 +9,12 @@ import App from './App'; if (typeof window !== 'undefined') { (window as any).React = React; (window as any).MaterialUI = MUI; - console.log( - '[index] Exposed React and MaterialUI to global scope for custom renderers', - ); + // Only log in development mode + if (import.meta.env.DEV || process.env.NODE_ENV === 'development') { + console.log( + '[index] Exposed React and MaterialUI to global scope for custom renderers', + ); + } } const root = ReactDOM.createRoot( diff --git a/formulus/src/api/synkronus/index.ts b/formulus/src/api/synkronus/index.ts index 17e7720a4..30260be49 100644 --- a/formulus/src/api/synkronus/index.ts +++ b/formulus/src/api/synkronus/index.ts @@ -154,6 +154,46 @@ class SynkronusApi { return response.data; } + /** + * Recursively removes a directory and all its contents. + * Handles nested directories and permission issues gracefully. + */ + private async removeDirectoryRecursive(path: string): Promise { + if (!(await RNFS.exists(path))) { + return; + } + + try { + const stat = await RNFS.stat(path); + if (stat.isDirectory()) { + try { + const items = await RNFS.readDir(path); + // Process items in reverse order to handle nested directories + for (let i = items.length - 1; i >= 0; i--) { + const item = items[i]; + const itemPath = `${path}/${item.name}`; + try { + if (item.isDirectory()) { + await this.removeDirectoryRecursive(itemPath); + } else { + await RNFS.unlink(itemPath); + } + } catch (itemError) { + // Continue with other items even if one fails + } + } + } catch (readDirError) { + // Continue to try removing the directory itself + } + } + await RNFS.unlink(path); + } catch (error) { + const errorMsg = + error instanceof Error ? error.message : String(error); + throw new Error(`Failed to clean directory ${path}: ${errorMsg}`); + } + } + /** * Downloads the app bundle as a single zip, extracts to a temp directory, * then atomically swaps into place so the old bundle stays intact until @@ -167,14 +207,33 @@ class SynkronusApi { this.fastGetToken_cachedToken ?? (await this.fastGetToken()); const zipUrl = `${config.basePath}/app-bundle/download-zip`; - const tempZipPath = `${RNFS.CachesDirectoryPath}/bundle_temp.zip`; - const tempExtractPath = `${RNFS.CachesDirectoryPath}/bundle_staging`; + // Use DocumentDirectoryPath for app data (more reliable than CachesDirectoryPath) + const tempZipPath = `${RNFS.DocumentDirectoryPath}/bundle_temp.zip`; + const tempExtractPath = `${RNFS.DocumentDirectoryPath}/bundle_staging`; const appDir = `${RNFS.DocumentDirectoryPath}/app`; const formsDir = `${RNFS.DocumentDirectoryPath}/forms`; // Clean up any leftover temp artifacts - if (await RNFS.exists(tempZipPath)) await RNFS.unlink(tempZipPath); - if (await RNFS.exists(tempExtractPath)) await RNFS.unlink(tempExtractPath); + if (await RNFS.exists(tempZipPath)) { + try { + await RNFS.unlink(tempZipPath); + } catch (error) { + // Non-fatal, continue + } + } + + // Use a unique path for each extraction to avoid conflicts + const timestamp = Date.now(); + const actualExtractPath = `${RNFS.DocumentDirectoryPath}/bundle_staging_${timestamp}`; + + // Try to clean up old staging directories (non-fatal) + if (await RNFS.exists(tempExtractPath)) { + try { + await this.removeDirectoryRecursive(tempExtractPath); + } catch (error) { + // Non-fatal - we're using a unique path anyway + } + } // Download the zip const downloadResult = await RNFS.downloadFile({ @@ -202,21 +261,74 @@ class SynkronusApi { progressCallback?.(50); + // Create fresh extract directory + try { + await RNFS.mkdir(actualExtractPath); + } catch (error) { + // Directory might already exist + if ( + !(error instanceof Error && error.message.includes('already exists')) + ) { + throw new Error( + `Failed to create extract directory: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + // Extract to staging directory - await RNFS.mkdir(tempExtractPath); - await unzip(tempZipPath, tempExtractPath); - progressCallback?.(80); + try { + await unzip(tempZipPath, actualExtractPath); + + // Verify extraction succeeded by checking if app directory exists + const stagingAppDir = `${actualExtractPath}/app`; + if (!(await RNFS.exists(stagingAppDir))) { + throw new Error( + 'Extraction completed but app directory not found in extracted files', + ); + } + + progressCallback?.(80); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + // Attempt to clean up the failed extraction directory + try { + await this.removeDirectoryRecursive(actualExtractPath); + } catch (cleanupError) { + // Non-fatal cleanup error + } + throw new Error( + `Failed to extract bundle: ${errorMsg}. This may indicate a corrupted ZIP file or permission issue.`, + ); + } // Atomic swap: remove old dirs, move staging content into place if (await RNFS.exists(appDir)) { - await RNFS.unlink(appDir); + try { + await this.removeDirectoryRecursive(appDir); + } catch (error) { + // Try direct unlink as fallback + try { + await RNFS.unlink(appDir); + } catch (unlinkError) { + // Both methods failed, continue anyway + } + } } if (await RNFS.exists(formsDir)) { - await RNFS.unlink(formsDir); + try { + await this.removeDirectoryRecursive(formsDir); + } catch (error) { + // Try direct unlink as fallback + try { + await RNFS.unlink(formsDir); + } catch (unlinkError) { + // Both methods failed, continue anyway + } + } } - const stagingAppDir = `${tempExtractPath}/app`; - const stagingFormsDir = `${tempExtractPath}/forms`; + const stagingAppDir = `${actualExtractPath}/app`; + const stagingFormsDir = `${actualExtractPath}/forms`; if (await RNFS.exists(stagingAppDir)) { await RNFS.moveFile(stagingAppDir, appDir); @@ -228,8 +340,20 @@ class SynkronusApi { progressCallback?.(95); // Clean up temp files - if (await RNFS.exists(tempZipPath)) await RNFS.unlink(tempZipPath); - if (await RNFS.exists(tempExtractPath)) await RNFS.unlink(tempExtractPath); + if (await RNFS.exists(tempZipPath)) { + try { + await RNFS.unlink(tempZipPath); + } catch (error) { + // Non-fatal cleanup error + } + } + if (await RNFS.exists(actualExtractPath)) { + try { + await this.removeDirectoryRecursive(actualExtractPath); + } catch (error) { + // Non-fatal cleanup error + } + } progressCallback?.(100); } diff --git a/synkronus/cmd/genhash/main.go b/synkronus/cmd/genhash/main.go index 5a32cc03b..fd80dca36 100644 --- a/synkronus/cmd/genhash/main.go +++ b/synkronus/cmd/genhash/main.go @@ -3,12 +3,17 @@ package main import ( "fmt" "log" + "os" "golang.org/x/crypto/bcrypt" ) func main() { + // If password provided as argument, use it; otherwise use defaults passwords := []string{"admin", "password123"} + if len(os.Args) > 1 { + passwords = os.Args[1:] + } for _, password := range passwords { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) From d8f227c93ade39ddb14875eccd6807315942c68a Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Thu, 26 Feb 2026 11:11:17 +0300 Subject: [PATCH 09/25] fix: resolve linting errors in synkronus index.ts - Prefix unused catch variables with _ to indicate intentional unused - Fix prettier formatting issues (remove extra whitespace) - All linting errors resolved for CI checks --- formulus/src/api/synkronus/index.ts | 31 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/formulus/src/api/synkronus/index.ts b/formulus/src/api/synkronus/index.ts index 30260be49..425311e0b 100644 --- a/formulus/src/api/synkronus/index.ts +++ b/formulus/src/api/synkronus/index.ts @@ -178,18 +178,17 @@ class SynkronusApi { } else { await RNFS.unlink(itemPath); } - } catch (itemError) { + } catch (_itemError) { // Continue with other items even if one fails } } - } catch (readDirError) { + } catch (_readDirError) { // Continue to try removing the directory itself } } await RNFS.unlink(path); } catch (error) { - const errorMsg = - error instanceof Error ? error.message : String(error); + const errorMsg = error instanceof Error ? error.message : String(error); throw new Error(`Failed to clean directory ${path}: ${errorMsg}`); } } @@ -217,7 +216,7 @@ class SynkronusApi { if (await RNFS.exists(tempZipPath)) { try { await RNFS.unlink(tempZipPath); - } catch (error) { + } catch (_error) { // Non-fatal, continue } } @@ -225,12 +224,12 @@ class SynkronusApi { // Use a unique path for each extraction to avoid conflicts const timestamp = Date.now(); const actualExtractPath = `${RNFS.DocumentDirectoryPath}/bundle_staging_${timestamp}`; - + // Try to clean up old staging directories (non-fatal) if (await RNFS.exists(tempExtractPath)) { try { await this.removeDirectoryRecursive(tempExtractPath); - } catch (error) { + } catch (_error) { // Non-fatal - we're using a unique path anyway } } @@ -278,7 +277,7 @@ class SynkronusApi { // Extract to staging directory try { await unzip(tempZipPath, actualExtractPath); - + // Verify extraction succeeded by checking if app directory exists const stagingAppDir = `${actualExtractPath}/app`; if (!(await RNFS.exists(stagingAppDir))) { @@ -286,14 +285,14 @@ class SynkronusApi { 'Extraction completed but app directory not found in extracted files', ); } - + progressCallback?.(80); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); // Attempt to clean up the failed extraction directory try { await this.removeDirectoryRecursive(actualExtractPath); - } catch (cleanupError) { + } catch (_cleanupError) { // Non-fatal cleanup error } throw new Error( @@ -305,11 +304,11 @@ class SynkronusApi { if (await RNFS.exists(appDir)) { try { await this.removeDirectoryRecursive(appDir); - } catch (error) { + } catch (_error) { // Try direct unlink as fallback try { await RNFS.unlink(appDir); - } catch (unlinkError) { + } catch (_unlinkError) { // Both methods failed, continue anyway } } @@ -317,11 +316,11 @@ class SynkronusApi { if (await RNFS.exists(formsDir)) { try { await this.removeDirectoryRecursive(formsDir); - } catch (error) { + } catch (_error) { // Try direct unlink as fallback try { await RNFS.unlink(formsDir); - } catch (unlinkError) { + } catch (_unlinkError) { // Both methods failed, continue anyway } } @@ -343,14 +342,14 @@ class SynkronusApi { if (await RNFS.exists(tempZipPath)) { try { await RNFS.unlink(tempZipPath); - } catch (error) { + } catch (_error) { // Non-fatal cleanup error } } if (await RNFS.exists(actualExtractPath)) { try { await this.removeDirectoryRecursive(actualExtractPath); - } catch (error) { + } catch (_error) { // Non-fatal cleanup error } } From 11f89af399ffbd88efad0c0a285e79b6b8211974 Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Tue, 17 Feb 2026 17:19:52 +0300 Subject: [PATCH 10/25] feat: secure custom question type loading via source extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace dynamic import() of file:// URIs with a sandboxed evaluation approach for custom question type modules. Security: - Add CustomQuestionTypeScanner (RN side) that reads index.js files as strings and screens them against a blocklist (fetch, XMLHttpRequest, eval, document.cookie, localStorage, etc.) - Rewrite CustomQuestionTypeLoader (WebView side) to evaluate source in a scoped sandbox via new Function(), exposing only React and MUI - Manifest shape changed from { modulePath: string } to { source: string } New files: - formulus/src/services/CustomQuestionTypeScanner.ts - formulus-formplayer/src/services/CustomQuestionTypeLoader.ts (rewritten) - formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts - formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx - formulus-formplayer/src/types/CustomQuestionTypeContract.ts - formulus-formplayer/docs/custom-question-types-architecture.md Modified files: - formulus/src/components/FormplayerModal.tsx (calls scanner) - FormulusInterfaceDefinition.ts (both projects, modulePath → source) - formulus-formplayer/src/App.tsx (orchestration) Signed-off-by: Jessie Ssebuliba --- formulus-formplayer/src/services/CustomQuestionTypeLoader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts index 4262b5f2e..fccca8007 100644 --- a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts +++ b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts @@ -178,7 +178,7 @@ export async function loadCustomQuestionTypes( if (result.errors.length > 0) { console.warn( `[CustomQuestionTypeLoader] ${result.errors.length} format(s) failed to load:`, - result.errors.map(e => e.format).join(', '), + result.errors.map((e) => e.format).join(', '), ); } From 0c1dc32b677cd5dcd93811b16f25959f85f6e945 Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Mon, 23 Feb 2026 08:54:08 +0300 Subject: [PATCH 11/25] clean up Signed-off-by: Jessie Ssebuliba --- .../custom-question-types-architecture.md | 602 ------------------ 1 file changed, 602 deletions(-) delete mode 100644 formulus-formplayer/docs/custom-question-types-architecture.md diff --git a/formulus-formplayer/docs/custom-question-types-architecture.md b/formulus-formplayer/docs/custom-question-types-architecture.md deleted file mode 100644 index 1fb24a8fd..000000000 --- a/formulus-formplayer/docs/custom-question-types-architecture.md +++ /dev/null @@ -1,602 +0,0 @@ -# Custom Question Types — Architecture & Flow - ---- - -## File Structure - -``` -formulus-formplayer/src/ (FORMPLAYER — runs in WebView) -├── types/ -│ ├── CustomQuestionTypeContract.ts ← 1. The contract authors code against -│ └── FormulusInterfaceDefinition.ts ← 2. FormInitData (carries the manifest) -│ -├── services/ -│ ├── CustomQuestionTypeLoader.ts ← 3. Sandboxed evaluation of source strings -│ └── CustomQuestionTypeRegistry.ts ← 4. Auto-generates testers + renderer entries -│ -├── renderers/ -│ └── CustomQuestionTypeAdapter.tsx ← 5. Bridges ControlProps → CustomQuestionTypeProps -│ -└── App.tsx ← 6. Orchestrates everything - -formulus/src/ (FORMULUS — runs in React Native) -├── services/ -│ └── CustomQuestionTypeScanner.ts ← Reads files, screens against blocklist -│ -└── components/ - └── FormplayerModal.tsx ← Calls scanner, passes source in FormInitData -``` - -### Author's Side (custom_app) - -``` -custom_app/ -└── question_types/ - ├── ranking/ - │ └── renderer.js ← default export: React component - ├── select-person/ - │ └── renderer.js - └── test-simple/ - └── renderer.js -``` - -**Note:** The folder name becomes the format string. For example: -- `question_types/ranking/renderer.js` → `format: "ranking"` -- `question_types/select-person/renderer.js` → `format: "select-person"` -- `question_types/test-simple/renderer.js` → `format: "test-simple"` - -**Important:** The scanner specifically looks for `renderer.js` (not `index.js`). The file must be named `renderer.js` in each question type directory. - ---- - -## Security Model — Source Extraction - -Custom question type JS files could contain malicious code. Instead of letting the WebView -`import()` arbitrary scripts (which would give them full access to fetch, DOM, localStorage, etc.), -we use a **source extraction** approach with two layers of defense: - -| Layer | Where | What it does | -|-------|-------|-------------| -| **Static blocklist** | RN side (`CustomQuestionTypeScanner`) | Rejects code containing dangerous patterns before it reaches the WebView | -| **Scoped evaluation** | WebView (`CustomQuestionTypeLoader`) | `new Function()` sandbox — code can only access React and MUI, nothing else | - -### Blocked Patterns (RN-side screening) - -``` -fetch( — network requests -XMLHttpRequest — network requests -WebSocket — persistent connections -eval( — dynamic code execution -new Function( — dynamic code execution -document.cookie — cookie access -localStorage — storage access -sessionStorage — storage access -indexedDB — database access -navigator.sendBeacon — data exfiltration -importScripts( — script injection -``` - -### Scoped Sandbox (WebView-side evaluation) - -```javascript -// Instead of: import("file:///path/to/index.js") -// We do: -const factory = new Function( - 'module', 'exports', 'React', 'MaterialUI', - sourceString // ← sent from RN as a string, not a file path -); - -// React and MaterialUI are accessed from global scope -const ReactLib = window.React || globalThis.React || self.React; -const MUILib = window.MaterialUI || globalThis.MaterialUI || self.MaterialUI; - -factory(moduleObj, exports, ReactLib, MUILib); - -// Custom code CAN access: React, MaterialUI, module, exports -// Custom code CANNOT access: fetch, document, localStorage, window, etc. -``` - -**Important Implementation Details:** -- React and MaterialUI are injected into the global scope by the WebView before custom question types are loaded -- The sandbox factory function receives these as explicit parameters, ensuring they're the only globals accessible -- If React or MaterialUI are not found in the global scope, loading fails with a clear error message -- The code supports both CommonJS patterns: `module.exports = Component` and `module.exports.default = Component` - ---- - -## How Module Loading Works - -### 1. Device Storage - -When the custom_app archive is unzipped, files land on the device filesystem: - -``` -/data/.../Documents/app/ -├── forms/ -│ ├── hh_hut/schema.json -│ ├── hh_person/schema.json -│ ├── p_focal/schema.json -│ └── ... -└── question_types/ - ├── ranking/renderer.js ← pairwise Elo ranking UI - ├── select-person/renderer.js ← person selection with search - └── test-simple/renderer.js ← simple test renderer -``` - -### 2. Formulus RN Scans, Reads & Screens - -`CustomQuestionTypeScanner.ts` scans `question_types/`, reads each `renderer.js` as a raw string, -and screens it against the blocklist: - -```typescript -// In CustomQuestionTypeScanner.ts -const questionTypesDir = `${customAppPath}/question_types`; -const folders = await RNFS.readDir(questionTypesDir); - -for (const folder of folders) { - if (folder.isDirectory()) { - const source = await RNFS.readFile(`${folder.path}/renderer.js`, 'utf8'); - - // Screen against blocklist - const violation = screenSource(source); - if (violation) { - errors.push({ name: folder.name, error: `Blocked: ${violation}` }); - continue; - } - - // Source is clean — include it - custom_types[folder.name] = { source }; - } -} -``` - -**Sample manifest** (source strings, not file paths): - -```json -{ - "custom_types": { - "ranking": { - "source": "(function() { 'use strict'; ... module.exports = { default: RankingRenderer }; })()" - }, - "select-person": { - "source": "(function() { 'use strict'; ... module.exports = { default: SelectPersonRenderer }; })()" - }, - "test-simple": { - "source": "(function() { 'use strict'; ... module.exports = { default: TestSimpleRenderer }; })()" - } - } -} -``` - -**Note:** The format name matches the folder name in `question_types/`. The scanner reads the file content and includes it as a source string in the manifest. - -### 3. FormInitData Carries the Source Strings - -In `FormplayerModal.tsx`, `initializeForm()` calls the scanner and includes the result: - -```typescript -const customAppPath = RNFS.DocumentDirectoryPath + '/app'; - -// Scan and screen custom question types -const scanResult = await scanCustomQuestionTypes(customAppPath); -if (scanResult.errors.length > 0) { - console.warn('Some custom question types failed screening:', scanResult.errors); -} - -const formInitData = { - formType: formType.id, - observationId, - params: formParams, - savedData: existingObservationData || {}, - formSchema: formType.schema, - uiSchema: formType.uiSchema ?? {}, - extensions, - customQuestionTypes: { - custom_types: scanResult.custom_types, - }, -} as FormInitData; -``` - -### 4. WebView Receives & Evaluates in Sandbox - -`FormulusWebViewHandler.sendFormInit()` serializes the `FormInitData` and injects it into -the WebView. Then `CustomQuestionTypeLoader.ts` evaluates each source in a scoped sandbox: - -```typescript -// CustomQuestionTypeLoader.ts — evaluateModuleInSandbox() -const exports: Record = {}; -const moduleObj = { exports }; - -// Access React and MaterialUI from global scope -const ReactLib = window.React || globalThis.React || self.React; -const MUILib = window.MaterialUI || globalThis.MaterialUI || self.MaterialUI; - -if (!ReactLib) { - throw new Error('React is not available in the global scope'); -} - -// Create factory with restricted scope -const factory = new Function( - 'module', 'exports', 'React', 'MaterialUI', - sourceString -); - -factory(moduleObj, exports, ReactLib, MUILib); - -// Extract the component (supports both default and named exports) -const component = moduleObj.exports.default ?? moduleObj.exports; - -// Validate it's a function -if (typeof component !== 'function') { - throw new Error(`Module does not export a valid React component`); -} -``` - -**Error Handling:** -- Each module evaluation is wrapped in try-catch -- Failed modules are logged with detailed error messages -- Loading continues for other modules even if one fails -- Errors are collected and returned in the result for debugging - -### 5. Registry & Rendering - -`CustomQuestionTypeRegistry.ts` takes each loaded component and: -- Auto-generates a tester: `rankWith(6, schemaMatches(s => s.format === name))` - - Priority 6 is higher than default Material renderers (3-5) but lower than specialized built-ins (10+) -- Creates a renderer entry via `CustomQuestionTypeAdapter.tsx` -- Returns renderer entries and format strings for AJV registration - -**Renderer Creation:** -- Each custom question type is wrapped in `QuestionShell` for consistent styling -- An `ErrorBoundary` catches any crashes in custom components -- The adapter maps JSON Forms `ControlProps` to simplified `CustomQuestionTypeProps` -- Config is extracted from schema properties (excluding reserved ones) and merged with `x-config` - -**AJV Format Registration:** -- Format strings are registered with AJV to prevent validation errors for unknown formats -- Registration happens in `App.tsx` after loading: `ajv.addFormat(formatName, () => true)` - ---- - -## Flow Diagram - -``` -┌─────────────────────────────────────────────────────────────┐ -│ DEVICE STORAGE (after custom_app unzip) │ -│ │ -│ /Documents/app/question_types/ranking/renderer.js │ -│ /Documents/app/question_types/select-person/renderer.js │ -│ /Documents/app/question_types/test-simple/renderer.js │ -└────────────────────────┬────────────────────────────────────┘ - │ RNFS.readFile() → string - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ FORMULUS RN — CustomQuestionTypeScanner │ -│ │ -│ 1. Reads each index.js as a raw string │ -│ 2. Screens against blocklist (fetch, eval, etc.) │ -│ 3. Builds manifest with source strings: │ -│ { "ranking": { source: "..." } } │ -│ 4. Rejected modules → logged as warnings │ -└────────────────────────┬────────────────────────────────────┘ - │ FormInitData.customQuestionTypes - │ sendFormInit() → injectJavaScript() - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ FORMPLAYER WEBVIEW — App.tsx │ -│ │ -│ initializeForm() reads initData.customQuestionTypes │ -│ calls loadCustomQuestionTypes(manifest) │ -└────────────────────────┬────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ CustomQuestionTypeLoader.ts — SANDBOX │ -│ │ -│ For each entry in manifest.custom_types: │ -│ 1. Access React/MaterialUI from global scope │ -│ 2. new Function('module','exports','React','MaterialUI',│ -│ source) │ -│ 3. Execute factory(moduleObj, exports, React, MUI) │ -│ 4. Extract module.exports.default or module.exports │ -│ 5. Validate it's a function │ -│ 6. Collect errors if evaluation fails │ -│ ❌ No access to: fetch, document, localStorage, etc. │ -└────────────────────────┬────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ CustomQuestionTypeRegistry.ts │ -│ │ -│ For each loaded component: │ -│ Auto-generates a tester: │ -│ rankWith(6, schemaMatches(s => s.format === name)) │ -│ Creates renderer entry via adapter │ -│ Returns: { renderers[], formats[] } │ -└────────────────────────┬────────────────────────────────────┘ - │ - ┌──────────┴──────────┐ - ▼ ▼ -┌──────────────────────┐ ┌──────────────────────────────────┐ -│ AJV Registration │ │ JsonForms Renderers Array │ -│ │ │ │ -│ ajv.addFormat( │ │ [ │ -│ 'ranking', │ │ ...builtInRenderers, │ -│ 'select-person', │ │ ...customTypeRenderers, ← NEW │ -│ () => true │ │ ] │ -│ ) │ │ ] │ -│ │ │ │ -│ Prevents AJV from │ │ Testers run top-to-bottom, │ -│ rejecting unknown │ │ highest rank wins │ -│ format strings │ │ │ -└──────────────────────┘ └───────────────┬──────────────────┘ - │ at render time - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ CustomQuestionTypeAdapter.tsx │ -│ │ -│ JSON Forms ControlProps → CustomQuestionTypeProps │ -│ ───────────────────── ──────────────────────── │ -│ data value │ -│ handleChange(path, val) onChange(val) │ -│ errors (string/array) validation { error, msg } │ -│ schema (all props) config (merged with x-config) │ -│ enabled enabled │ -│ path fieldPath │ -│ label, description label, description │ -│ │ -│ Config extraction: │ -│ - All schema properties (except reserved) → config │ -│ - x-config properties override schema properties │ -│ - Reserved: type, title, description, format, enum, etc.│ -│ │ -│ Wraps in: QuestionShell + ErrorBoundary │ -│ - ErrorBoundary shows user-friendly error UI │ -│ - Form continues to function if component crashes │ -└────────────────────────┬────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Author's Component │ -│ │ -│ Receives only: { value, config, onChange, validation, ... }│ -│ No JSON Forms knowledge needed. │ -│ Crash-safe via ErrorBoundary. │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Schema Examples (based on AnthroCollect forms) - -### Ranking Question - -Used for pairwise comparison ranking of people (similar to ODK-X OMO style): - -```json -{ - "ranking_field": { - "type": "array", - "format": "ranking", - "title": "Rank People", - "description": "Rank the people in order of preference", - "items": { - "type": "string" - }, - "people": [ - { - "id": "person1", - "name": "John Doe", - "age": 35, - "clan": "Alpha", - "sex": "male", - "photo_uriFragment": null - }, - { - "id": "person2", - "name": "Jane Smith", - "age": 28, - "clan": "Beta", - "sex": "female", - "photo_uriFragment": null - } - ], - "promptText": "Select the person you prefer" - } -} -``` - -**Key points:** -- `format: "ranking"` (no "x-" prefix needed) -- `people` array is passed directly in schema (becomes `config.people`) -- `promptText` is optional and becomes `config.promptText` -- Value stored is an array of person IDs in ranked order -- Uses Elo-style pairwise comparison algorithm - -### Select Person Question - -Used for selecting a person from a list with optional search: - -```json -{ - "select_person_field": { - "type": "string", - "format": "select-person", - "title": "Select Person", - "description": "Choose a person from the list", - "showSearch": true, - "showPhoto": false, - "people": [ - { - "id": "person1", - "name": "John Doe", - "age": 35, - "clan": "Alpha", - "sex": "male" - }, - { - "id": "person2", - "name": "Jane Smith", - "age": 28, - "clan": "Beta", - "sex": "female" - } - ] - } -} -``` - -**Key points:** -- `format: "select-person"` (no "x-" prefix needed) -- `people` array is passed directly in schema (becomes `config.people`) -- `showSearch` (default: true) enables searchable autocomplete -- `showPhoto` (default: false) shows person photos if available -- Value stored is the selected person's ID (string) - -### Simple Test Question - -Minimal example for testing the custom question type system: - -```json -{ - "test_custom_field": { - "type": "string", - "format": "test-simple", - "title": "Test Custom Question Type", - "description": "This field uses the test-simple custom question type renderer", - "placeholder": "Enter test value here...", - "maxLength": 50 - } -} -``` - -**Key points:** -- `format: "test-simple"` (no "x-" prefix needed) -- `placeholder` is passed directly in schema (becomes `config.placeholder`) -- Standard JSON Schema validation (`maxLength`) still applies - -### Using x-config (Alternative) - -You can also use `x-config` for explicit configuration that overrides schema properties: - -```json -{ - "custom_field": { - "type": "string", - "format": "my-custom-type", - "title": "My Field", - "maxLength": 100, - "x-config": { - "customParam": "value", - "maxLength": 200 - } - } -} -``` - -In this case, `config.maxLength` will be `200` (from `x-config`), not `100` (from schema property). - -**What happens for each:** - -1. `format: "ranking"` → tester matches → the ranking renderer is used -2. Schema properties (except reserved ones) → passed as `props.config` to the author's component -3. `x-config` properties → override schema properties in config -4. Standard JSON Schema keywords (`type`, `maxLength`, etc.) → validated by AJV as normal -5. AJV doesn't reject the custom format strings because we registered them - ---- - -## Implementation Details - -### Security Model Implementation - -**Source Extraction Flow:** -1. RN side (`CustomQuestionTypeScanner`) reads JS files as raw strings -2. Static blocklist screening rejects dangerous patterns (fetch, eval, localStorage, etc.) -3. Clean source strings are passed in `FormInitData.customQuestionTypes` -4. WebView side (`CustomQuestionTypeLoader`) evaluates source in scoped sandbox -5. Only React, MaterialUI, module, and exports are accessible to custom code - -**Global Scope Access:** -- React and MaterialUI must be available in the WebView's global scope before loading -- The loader checks `window`, `globalThis`, and `self` for these libraries -- If not found, loading fails with a clear error message -- This ensures custom code can use React hooks and Material UI components - -**Error Handling:** -- Each module evaluation is wrapped in try-catch -- Failed modules are logged but don't stop other modules from loading -- Errors are collected and returned: `{ format: string, error: string }[]` -- The registry only processes successfully loaded components - -### Module Export Patterns - -Custom question type modules can export components in multiple ways: -```javascript -// Pattern 1: Object with default property (recommended) -module.exports = { - default: function MyComponent(props) { ... } -}; - -// Pattern 2: Direct default export -module.exports.default = function MyComponent(props) { ... }; - -// Pattern 3: Direct export (also supported) -module.exports = function MyComponent(props) { ... }; - -// Pattern 4: Named export (also supported) -module.exports.MyComponent = function MyComponent(props) { ... }; -``` - -The loader checks `module.exports.default` first, then falls back to `module.exports`. The recommended pattern is Pattern 1 (object with default property) as used in the AnthroCollect examples. - -### Config Extraction - -The adapter extracts configuration from the schema: -- All schema properties except reserved ones become `config` -- Reserved properties: `type`, `title`, `description`, `format`, `enum`, `const`, `default`, `required`, `properties`, `items`, `oneOf`, `anyOf`, `allOf`, `$ref`, `$schema`, validation keywords, etc. -- Properties from `x-config` override schema properties -- This allows passing parameters like `maxStars: 5` directly in the schema - -### Tester Priority - -Custom question type testers use priority 6: -- Higher than default Material renderers (priority 3-5) -- Lower than specialized built-in question types (priority 10+) -- Ensures custom types are selected when format matches, but built-ins take precedence for their specific formats - -## Implementation Plan (completed) - -All changes below have been implemented. - -### Formulus RN Side - -| File | Change | -|------|--------| -| `FormulusInterfaceDefinition.ts` | `modulePath` → `source` in `FormInitData.customQuestionTypes` | -| `CustomQuestionTypeScanner.ts` | **NEW** — scans, reads, screens question type modules | -| `FormplayerModal.tsx` | Calls scanner, passes source strings in `FormInitData` | - -### FormPlayer WebView Side - -| File | Change | -|------|--------| -| `FormulusInterfaceDefinition.ts` | `modulePath` → `source` (mirror) | -| `CustomQuestionTypeContract.ts` | `modulePath` → `source` in `CustomQuestionTypeManifest` | -| `CustomQuestionTypeLoader.ts` | Rewritten: `import()` → `new Function()` sandbox with React/MUI from global scope | -| `CustomQuestionTypeRegistry.ts` | Auto-generates testers with priority 6, creates renderer entries | -| `CustomQuestionTypeAdapter.tsx` | Maps ControlProps → CustomQuestionTypeProps, wraps in ErrorBoundary | -| `App.tsx` | Orchestrates loading, registers formats with AJV, merges with built-in renderers | - -### Key Files — Full Reference - -| File | Role | Key Export | -|------|------|-----------| -| `CustomQuestionTypeScanner.ts` (RN) | Reads & screens modules | `scanCustomQuestionTypes()` | -| `CustomQuestionTypeContract.ts` | Defines what authors receive | `CustomQuestionTypeProps` | -| `CustomQuestionTypeLoader.ts` | Sandboxed evaluation | `loadCustomQuestionTypes()` | -| `CustomQuestionTypeRegistry.ts` | Creates JsonForms entries | `registerCustomQuestionTypes()` | -| `CustomQuestionTypeAdapter.tsx` | Props bridge + error isolation | `createCustomQuestionTypeRenderer()` | -| `FormulusInterfaceDefinition.ts` | Carries source from RN → WebView | `FormInitData` | -| `App.tsx` | Orchestrates load → register → render | `initializeForm()` | -| `FormplayerModal.tsx` (RN) | Builds FormInitData, sends to WebView | `initializeForm()` | From 5575c116316626b2418bc7c8c54e63a396380d6f Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Tue, 24 Feb 2026 14:02:36 +0300 Subject: [PATCH 12/25] remove security scanning of renderers Signed-off-by: Jessie Ssebuliba --- .../src/types/CustomQuestionTypeContract.ts | 7 +++--- .../src/types/FormulusInterfaceDefinition.ts | 2 +- formulus/src/components/FormplayerModal.tsx | 23 +------------------ .../webview/FormulusInterfaceDefinition.ts | 2 +- 4 files changed, 6 insertions(+), 28 deletions(-) diff --git a/formulus-formplayer/src/types/CustomQuestionTypeContract.ts b/formulus-formplayer/src/types/CustomQuestionTypeContract.ts index 7a46ca806..09327e487 100644 --- a/formulus-formplayer/src/types/CustomQuestionTypeContract.ts +++ b/formulus-formplayer/src/types/CustomQuestionTypeContract.ts @@ -53,15 +53,14 @@ export interface CustomQuestionTypeProps { /** * Manifest passed from the native side describing available custom question types. - * Each entry maps a format string to the source code of the module that renders it. - * The RN side reads the JS file and passes the source string here for sandboxed evaluation. + * Each entry maps a format string to the path of the module that renders it. */ export interface CustomQuestionTypeManifest { custom_types: Record< string, { - /** The JS source code of the module (read by RN via RNFS.readFile) */ - source: string; + /** Path to the JS module (e.g., "file:///path/to/question_types/rating-stars/index.js") */ + modulePath: string; } >; } diff --git a/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts b/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts index e048cd8dc..d63ad3cf3 100644 --- a/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts +++ b/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts @@ -58,7 +58,7 @@ export interface FormInitData { operationId?: string; extensions?: ExtensionMetadata; customQuestionTypes?: { - custom_types: Record; + custom_types: Record; }; } diff --git a/formulus/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx index cc09969eb..8ee736e6b 100644 --- a/formulus/src/components/FormplayerModal.tsx +++ b/formulus/src/components/FormplayerModal.tsx @@ -35,7 +35,6 @@ import { databaseService } from '../database'; import { colors } from '../theme/colors'; import { FormSpec } from '../services'; // FormService will be imported directly import { ExtensionService } from '../services/ExtensionService'; -import { scanCustomQuestionTypes } from '../services/CustomQuestionTypeScanner'; import RNFS from 'react-native-fs'; import { useAppTheme } from '../contexts/AppThemeContext'; import { geolocationService } from '../services/GeolocationService'; @@ -240,9 +239,9 @@ const FormplayerModal = forwardRef( }; // Load extensions for this form - const customAppPath = RNFS.DocumentDirectoryPath + '/app'; let extensions = undefined; try { + const customAppPath = RNFS.DocumentDirectoryPath + '/app'; const extensionService = ExtensionService.getInstance(); const mergedExtensions = await extensionService.getCustomAppExtensions( customAppPath, @@ -314,25 +313,6 @@ const FormplayerModal = forwardRef( return; } - // Scan custom question types (reads JS files, screens against blocklist) - let customQuestionTypes = undefined; - try { - const scanResult = await scanCustomQuestionTypes(customAppPath); - if (Object.keys(scanResult.custom_types).length > 0) { - customQuestionTypes = { - custom_types: scanResult.custom_types, - }; - } - if (scanResult.errors.length > 0) { - console.warn( - 'Some custom question types failed screening:', - scanResult.errors, - ); - } - } catch (error) { - console.warn('Failed to scan custom question types:', error); - } - const formInitData = { formType: formType.id, observationId: observationId, @@ -341,7 +321,6 @@ const FormplayerModal = forwardRef( formSchema: formType.schema, uiSchema: formType.uiSchema ?? {}, extensions, - customQuestionTypes, } as FormInitData; if (!webViewRef.current) { diff --git a/formulus/src/webview/FormulusInterfaceDefinition.ts b/formulus/src/webview/FormulusInterfaceDefinition.ts index e048cd8dc..d63ad3cf3 100644 --- a/formulus/src/webview/FormulusInterfaceDefinition.ts +++ b/formulus/src/webview/FormulusInterfaceDefinition.ts @@ -58,7 +58,7 @@ export interface FormInitData { operationId?: string; extensions?: ExtensionMetadata; customQuestionTypes?: { - custom_types: Record; + custom_types: Record; }; } From a11ec3c936623f037d717831e2f05e3da458ea60 Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Thu, 26 Feb 2026 10:16:33 +0300 Subject: [PATCH 13/25] scan question_types folder for custom question types and create a manifest Signed-off-by: Jessie Ssebuliba --- formulus/src/components/FormplayerModal.tsx | 35 ++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/formulus/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx index 8ee736e6b..ccbaf4b44 100644 --- a/formulus/src/components/FormplayerModal.tsx +++ b/formulus/src/components/FormplayerModal.tsx @@ -239,9 +239,9 @@ const FormplayerModal = forwardRef( }; // Load extensions for this form + const customAppPath = RNFS.DocumentDirectoryPath + '/app'; let extensions = undefined; try { - const customAppPath = RNFS.DocumentDirectoryPath + '/app'; const extensionService = ExtensionService.getInstance(); const mergedExtensions = await extensionService.getCustomAppExtensions( customAppPath, @@ -313,6 +313,38 @@ const FormplayerModal = forwardRef( return; } + // Scan custom question types (reads directories, builds modulePath manifest) + let customQuestionTypes = undefined; + try { + const qtDir = `${customAppPath}/forms/question_types`; + const qtDirExists = await RNFS.exists(qtDir); + if (qtDirExists) { + const folders = await RNFS.readDir(qtDir); + const custom_types: Record = {}; + for (const folder of folders) { + if (folder.isDirectory()) { + const indexPath = `${folder.path}/index.js`; + if (await RNFS.exists(indexPath)) { + custom_types[folder.name] = { + modulePath: `file://${indexPath}`, + }; + console.log( + `[FormplayerModal] Found custom question type: ${folder.name}`, + ); + } + } + } + if (Object.keys(custom_types).length > 0) { + customQuestionTypes = { custom_types }; + console.log( + `[FormplayerModal] Loaded ${Object.keys(custom_types).length} custom question type(s)`, + ); + } + } + } catch (error) { + console.warn('Failed to scan custom question types:', error); + } + const formInitData = { formType: formType.id, observationId: observationId, @@ -321,6 +353,7 @@ const FormplayerModal = forwardRef( formSchema: formType.schema, uiSchema: formType.uiSchema ?? {}, extensions, + customQuestionTypes, } as FormInitData; if (!webViewRef.current) { From 705b4273a5eff610a18fa3fc7a3fb643a0483ff1 Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Thu, 26 Feb 2026 11:54:07 +0300 Subject: [PATCH 14/25] use sandboxing since renderers are in commonjs and the formulus is in ES6 syntax Signed-off-by: Jessie Ssebuliba --- .../src/types/FormulusInterfaceDefinition.ts | 2 +- formulus/src/components/FormplayerModal.tsx | 73 ++++++++++++++----- formulus/src/services/FormService.ts | 8 +- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts b/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts index d63ad3cf3..e048cd8dc 100644 --- a/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts +++ b/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts @@ -58,7 +58,7 @@ export interface FormInitData { operationId?: string; extensions?: ExtensionMetadata; customQuestionTypes?: { - custom_types: Record; + custom_types: Record; }; } diff --git a/formulus/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx index ccbaf4b44..98ea8fbba 100644 --- a/formulus/src/components/FormplayerModal.tsx +++ b/formulus/src/components/FormplayerModal.tsx @@ -313,33 +313,72 @@ const FormplayerModal = forwardRef( return; } - // Scan custom question types (reads directories, builds modulePath manifest) + // Scan custom question types and read their source code + // Check both root forms/ and app/forms/ paths (same dual-path as FormService) let customQuestionTypes = undefined; try { - const qtDir = `${customAppPath}/forms/question_types`; - const qtDirExists = await RNFS.exists(qtDir); - if (qtDirExists) { + const qtDirs = [ + RNFS.DocumentDirectoryPath + '/forms/question_types', + `${customAppPath}/forms/question_types`, + ]; + console.log( + `🔍🔍🔍 [FormplayerModal] Scanning custom question types in: ${qtDirs.join(', ')}`, + ); + + const custom_types: Record = {}; + + for (const qtDir of qtDirs) { + const qtDirExists = await RNFS.exists(qtDir); + if (!qtDirExists) { + console.log( + `🔍 [FormplayerModal] Path not found, skipping: ${qtDir}`, + ); + continue; + } + const folders = await RNFS.readDir(qtDir); - const custom_types: Record = {}; + console.log( + `🔍 [FormplayerModal] Found ${folders.length} items in ${qtDir}: ${folders.map(f => f.name).join(', ')}`, + ); + for (const folder of folders) { - if (folder.isDirectory()) { + if (folder.isDirectory() && !custom_types[folder.name]) { + // Try renderer.js first, then index.js as fallback + const rendererPath = `${folder.path}/renderer.js`; const indexPath = `${folder.path}/index.js`; - if (await RNFS.exists(indexPath)) { - custom_types[folder.name] = { - modulePath: `file://${indexPath}`, - }; + const hasRenderer = await RNFS.exists(rendererPath); + const hasIndex = !hasRenderer && (await RNFS.exists(indexPath)); + const jsPath = hasRenderer + ? rendererPath + : hasIndex + ? indexPath + : null; + + if (jsPath) { + // Read the source code so the WebView can evaluate it directly + const source = await RNFS.readFile(jsPath, 'utf8'); + custom_types[folder.name] = { source }; console.log( - `[FormplayerModal] Found custom question type: ${folder.name}`, + `[FormplayerModal] Custom question type: "${folder.name}" (${source.length} bytes from ${jsPath})`, + ); + } else { + console.warn( + `⚠️ [FormplayerModal] Skipping "${folder.name}": no renderer.js or index.js found`, ); } } } - if (Object.keys(custom_types).length > 0) { - customQuestionTypes = { custom_types }; - console.log( - `[FormplayerModal] Loaded ${Object.keys(custom_types).length} custom question type(s)`, - ); - } + } + + if (Object.keys(custom_types).length > 0) { + customQuestionTypes = { custom_types }; + console.log( + `📦📦📦 [FormplayerModal] Custom question types manifest: ${JSON.stringify(Object.keys(custom_types))}`, + ); + } else { + console.warn( + '⚠️ [FormplayerModal] No custom question types found in any path', + ); } } catch (error) { console.warn('Failed to scan custom question types:', error); diff --git a/formulus/src/services/FormService.ts b/formulus/src/services/FormService.ts index 917885684..32d356edf 100644 --- a/formulus/src/services/FormService.ts +++ b/formulus/src/services/FormService.ts @@ -115,11 +115,12 @@ export class FormService { } const formSpecFolders = await RNFS.readDir(formSpecsDir); - // Skip non-form directories (e.g. extensions/, question_types/, .hidden) + // Skip non-form directories (e.g. extensions/, question_types/, .hidden, temp_*) const formDirs = formSpecFolders.filter( f => f.isDirectory() && !f.name.startsWith('.') && + !f.name.startsWith('temp_') && f.name !== 'extensions' && f.name !== 'question_types', ); @@ -142,7 +143,10 @@ export class FormService { } console.log( - `FormService: Successfully loaded ${allFormSpecs.length} form specs`, + `🟢🟢🟢 [FormService] Successfully loaded ${allFormSpecs.length} form specs`, + ); + console.log( + `🟢 [FormService] Form IDs: ${allFormSpecs.map(f => f.id).join(', ')}`, ); return allFormSpecs; } catch (error) { From 7cda15069b5c04d14e19230b19f5dda42c0913aa Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Thu, 26 Feb 2026 12:06:52 +0300 Subject: [PATCH 15/25] fix build errors Signed-off-by: Jessie Ssebuliba --- formulus/src/webview/FormulusInterfaceDefinition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formulus/src/webview/FormulusInterfaceDefinition.ts b/formulus/src/webview/FormulusInterfaceDefinition.ts index d63ad3cf3..e048cd8dc 100644 --- a/formulus/src/webview/FormulusInterfaceDefinition.ts +++ b/formulus/src/webview/FormulusInterfaceDefinition.ts @@ -58,7 +58,7 @@ export interface FormInitData { operationId?: string; extensions?: ExtensionMetadata; customQuestionTypes?: { - custom_types: Record; + custom_types: Record; }; } From a74f8680d64ccdb94a9bb063427241e81d0cf19d Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Thu, 26 Feb 2026 13:31:47 +0300 Subject: [PATCH 16/25] pass the jsonform context to the renderers Signed-off-by: Jessie Ssebuliba --- docs/custom-question-types.md | 137 ++++++++++++++++++ .../src/types/CustomQuestionTypeContract.ts | 6 + 2 files changed, 143 insertions(+) create mode 100644 docs/custom-question-types.md diff --git a/docs/custom-question-types.md b/docs/custom-question-types.md new file mode 100644 index 000000000..b50bd19e2 --- /dev/null +++ b/docs/custom-question-types.md @@ -0,0 +1,137 @@ +# Custom Question Types in Formplayer + +Formplayer supports a plugin system that allows you to render custom UI components for specific questions in your forms. This is useful for complex interactions that standard JSON Forms inputs cannot handle (e.g., drag-and-drop ranking, signature pads, or complex search interfaces). + +## How it Works + +1. **Folder Structure**: You place your custom component inside the `/forms/question_types//` directory of your `custom_app`. +2. **Implementation**: You create a JavaScript file (`renderer.js` or `index.js`) that exports a React component. +3. **Usage in Schema**: You set the `"format"` property in your `schema.json` to match the `` folder. +4. **Loading Payload**: When Formulus opens a form, it reads all custom question types in the `question_types` folder and injects their source code into the Formplayer WebView. +5. **Execution**: Formplayer dynamically evaluates and registers your component. It wraps it in an error boundary and adapts it to the JSONForms architecture. + +--- + +## Creating a Custom Question Type + +### 1. File Location + +Create a folder named after your custom format (e.g., `ranking`). Inside it, create `renderer.js`. + +``` +custom_app/ + forms/ + question_types/ + ranking/ + renderer.js <-- Your component +``` + +### 2. The Component Interface + +Your `renderer.js` must **default export** a React component (CommonJS style `module.exports = { default: Component }`). It receives the following props: + +```ts +interface CustomQuestionTypeProps { + // 1. Current field data + value: any; + + // 2. The callback to update the form's data state. Must be called when user changes value. + onChange: (newValue: any) => void; + + // 3. Schema parameters. Any non-standard JSON Schema keys are passed here. + config: Record; + + // 4. Validation state for styling error cases + validation: { + error: boolean; + message: string; + }; + + // 5. Context from JSON Forms to access the whole form data/schema + jsonFormsContext: { + core: { + data: any; // The entire form's current data + schema: any; // The root schema + errors: any[]; // All validation errors + }; + // ...other JSONForms state + }; + + // 6. UI properties + enabled: boolean; + label: string; + description?: string; + fieldPath: string; // The dot-notation path in the data (e.g. "team.ranking") +} +``` + +### 3. Example Implementation: `renderer.js` + +Because the Formplayer evaluates this at runtime within a browser without a bundler, you cannot rely on ES modules (`import/export`). Instead, use CommonJS (`module.exports`) and rely on injected globals like `React` and `MaterialUI`. + +```javascript +const { useState, useEffect } = React; +const { Button, Typography, Box } = MaterialUI; + +function MyCustomRenderer(props) { + const { value, onChange, config, validation, label } = props; + + // "config" contains any extra properties from your schema.json + const maxItems = config.maxItems || 5; + + return ( + + {label} + Current Value: {JSON.stringify(value)} + + + + {validation.error && ( + {validation.message} + )} + + ); +} + +// Emulate an ES Module default export for the loader +module.exports = { + default: MyCustomRenderer, +}; +``` + +--- + +## Using it in your Form + +Once your custom question type is defined, use it in your form's `schema.json`. + +By default, standard JSON Schema properties (`type`, `title`, `description`, `required`, etc.) are consumed by the core engine. **Any other custom properties** inside the field definition will be passed to your component inside the `config` prop! + +```json +{ + "type": "string", + "title": "Select a Person", + "format": "select-person", <-- Matches the folder name + + "endpoint": "/api/v1/people", <-- Passed to props.config.endpoint + "showSearch": true, <-- Passed to props.config.showSearch + "theme": "dark" <-- Passed to props.config.theme +} +``` + +### Data Storage + +Your component can return complex objects, arrays, or primitive values back to `onChange()`. Just ensure the `"type"` property in your schema matches what you are returning (e.g. `"type": "object"` if returning an object) so that AJV validation passes. + +--- + +## Error Handling + +If your custom component throws an exception or crashes while rendering, Formplayer will catch it and display a red fallback UI in place of your question. This ensures that a bug in one custom question does not break the entire form or block the user from answering other questions. diff --git a/formulus-formplayer/src/types/CustomQuestionTypeContract.ts b/formulus-formplayer/src/types/CustomQuestionTypeContract.ts index 09327e487..1053d9de7 100644 --- a/formulus-formplayer/src/types/CustomQuestionTypeContract.ts +++ b/formulus-formplayer/src/types/CustomQuestionTypeContract.ts @@ -49,6 +49,12 @@ export interface CustomQuestionTypeProps { /** Optional description from the schema's `description` property */ description?: string; + /** + * JSONForms context context if required. + * Provides access to the whole form's `core.data` (all values in the form), + * `core.schema` (the root schema), and other global JSONForms state. + */ + jsonFormsContext?: any; } /** From 1a373e4437712ffa20a384675436be7575258032 Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Tue, 17 Feb 2026 17:19:52 +0300 Subject: [PATCH 17/25] feat: secure custom question type loading via source extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace dynamic import() of file:// URIs with a sandboxed evaluation approach for custom question type modules. Security: - Add CustomQuestionTypeScanner (RN side) that reads index.js files as strings and screens them against a blocklist (fetch, XMLHttpRequest, eval, document.cookie, localStorage, etc.) - Rewrite CustomQuestionTypeLoader (WebView side) to evaluate source in a scoped sandbox via new Function(), exposing only React and MUI - Manifest shape changed from { modulePath: string } to { source: string } New files: - formulus/src/services/CustomQuestionTypeScanner.ts - formulus-formplayer/src/services/CustomQuestionTypeLoader.ts (rewritten) - formulus-formplayer/src/services/CustomQuestionTypeRegistry.ts - formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx - formulus-formplayer/src/types/CustomQuestionTypeContract.ts - formulus-formplayer/docs/custom-question-types-architecture.md Modified files: - formulus/src/components/FormplayerModal.tsx (calls scanner) - FormulusInterfaceDefinition.ts (both projects, modulePath → source) - formulus-formplayer/src/App.tsx (orchestration) Signed-off-by: Jessie Ssebuliba --- .../custom-question-types-architecture.md | 404 ++++++++++++++++++ formulus/src/components/FormplayerModal.tsx | 71 +-- 2 files changed, 414 insertions(+), 61 deletions(-) create mode 100644 formulus-formplayer/docs/custom-question-types-architecture.md diff --git a/formulus-formplayer/docs/custom-question-types-architecture.md b/formulus-formplayer/docs/custom-question-types-architecture.md new file mode 100644 index 000000000..bb53d1cab --- /dev/null +++ b/formulus-formplayer/docs/custom-question-types-architecture.md @@ -0,0 +1,404 @@ +# Custom Question Types — Architecture & Flow + +--- + +## File Structure + +``` +formulus-formplayer/src/ (FORMPLAYER — runs in WebView) +├── types/ +│ ├── CustomQuestionTypeContract.ts ← 1. The contract authors code against +│ └── FormulusInterfaceDefinition.ts ← 2. FormInitData (carries the manifest) +│ +├── services/ +│ ├── CustomQuestionTypeLoader.ts ← 3. Sandboxed evaluation of source strings +│ └── CustomQuestionTypeRegistry.ts ← 4. Auto-generates testers + renderer entries +│ +├── renderers/ +│ └── CustomQuestionTypeAdapter.tsx ← 5. Bridges ControlProps → CustomQuestionTypeProps +│ +└── App.tsx ← 6. Orchestrates everything + +formulus/src/ (FORMULUS — runs in React Native) +├── services/ +│ └── CustomQuestionTypeScanner.ts ← Reads files, screens against blocklist +│ +└── components/ + └── FormplayerModal.tsx ← Calls scanner, passes source in FormInitData +``` + +### Author's Side (custom_app) + +``` +custom_app/ +└── question_types/ + ├── x-ranking/ + │ └── index.js ← default export: React component + ├── x-dynamicEnum/ + │ └── index.js + └── x-custom-text/ + └── index.js +``` + +--- + +## Security Model — Source Extraction + +Custom question type JS files could contain malicious code. Instead of letting the WebView +`import()` arbitrary scripts (which would give them full access to fetch, DOM, localStorage, etc.), +we use a **source extraction** approach with two layers of defense: + +| Layer | Where | What it does | +|-------|-------|-------------| +| **Static blocklist** | RN side (`CustomQuestionTypeScanner`) | Rejects code containing dangerous patterns before it reaches the WebView | +| **Scoped evaluation** | WebView (`CustomQuestionTypeLoader`) | `new Function()` sandbox — code can only access React and MUI, nothing else | + +### Blocked Patterns (RN-side screening) + +``` +fetch( — network requests +XMLHttpRequest — network requests +WebSocket — persistent connections +eval( — dynamic code execution +new Function( — dynamic code execution +document.cookie — cookie access +localStorage — storage access +sessionStorage — storage access +indexedDB — database access +navigator.sendBeacon — data exfiltration +importScripts( — script injection +``` + +### Scoped Sandbox (WebView-side evaluation) + +```javascript +// Instead of: import("file:///path/to/index.js") +// We do: +const factory = new Function( + 'module', 'exports', 'React', 'MaterialUI', + sourceString // ← sent from RN as a string, not a file path +); + +// Custom code CAN access: React, MaterialUI, module, exports +// Custom code CANNOT access: fetch, document, localStorage, window, etc. +``` + +--- + +## How Module Loading Works + +### 1. Device Storage + +When the custom_app archive is unzipped, files land on the device filesystem: + +``` +/data/.../Documents/app/ +├── forms/ +│ ├── hh_hut/schema.json +│ ├── hh_person/schema.json +│ ├── p_focal/schema.json +│ └── ... +└── question_types/ + ├── x-ranking/index.js ← pairwise Elo ranking UI + ├── x-dynamicEnum/index.js ← dynamic choice list from DB queries + └── x-custom-text/index.js ← enhanced text input +``` + +### 2. Formulus RN Scans, Reads & Screens + +`CustomQuestionTypeScanner.ts` scans `question_types/`, reads each `index.js` as a raw string, +and screens it against the blocklist: + +```typescript +// In CustomQuestionTypeScanner.ts +const questionTypesDir = `${customAppPath}/question_types`; +const folders = await RNFS.readDir(questionTypesDir); + +for (const folder of folders) { + if (folder.isDirectory()) { + const source = await RNFS.readFile(`${folder.path}/index.js`, 'utf8'); + + // Screen against blocklist + const violation = screenSource(source); + if (violation) { + errors.push({ name: folder.name, error: `Blocked: ${violation}` }); + continue; + } + + // Source is clean — include it + custom_types[folder.name] = { source }; + } +} +``` + +**Sample manifest** (source strings, not file paths): + +```json +{ + "custom_types": { + "x-ranking": { + "source": "(function() { 'use strict'; ... module.exports = RankingRenderer; })()" + }, + "x-dynamicEnum": { + "source": "(function() { 'use strict'; ... module.exports = DynamicEnumControl; })()" + }, + "x-custom-text": { + "source": "(function() { 'use strict'; ... module.exports = CustomTextRenderer; })()" + } + } +} +``` + +### 3. FormInitData Carries the Source Strings + +In `FormplayerModal.tsx`, `initializeForm()` calls the scanner and includes the result: + +```typescript +const customAppPath = RNFS.DocumentDirectoryPath + '/app'; + +// Scan and screen custom question types +const scanResult = await scanCustomQuestionTypes(customAppPath); +if (scanResult.errors.length > 0) { + console.warn('Some custom question types failed screening:', scanResult.errors); +} + +const formInitData = { + formType: formType.id, + observationId, + params: formParams, + savedData: existingObservationData || {}, + formSchema: formType.schema, + uiSchema: formType.uiSchema ?? {}, + extensions, + customQuestionTypes: { + custom_types: scanResult.custom_types, + }, +} as FormInitData; +``` + +### 4. WebView Receives & Evaluates in Sandbox + +`FormulusWebViewHandler.sendFormInit()` serializes the `FormInitData` and injects it into +the WebView. Then `CustomQuestionTypeLoader.ts` evaluates each source in a scoped sandbox: + +```typescript +// CustomQuestionTypeLoader.ts — evaluateModuleInSandbox() +const exports = {}; +const moduleObj = { exports }; + +const factory = new Function( + 'module', 'exports', 'React', 'MaterialUI', + meta.source, +); + +factory(moduleObj, exports, React, MaterialUI); + +// Extract only the component +const component = moduleObj.exports.default ?? moduleObj.exports; +``` + +### 5. Registry & Rendering + +`CustomQuestionTypeRegistry.ts` takes each loaded component and: +- Auto-generates a tester: `rankWith(6, schemaMatches(s => s.format === name))` +- Creates a renderer entry via `CustomQuestionTypeAdapter.tsx` +- Registers the format with AJV: `ajv.addFormat('x-ranking', () => true)` + +--- + +## Flow Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ DEVICE STORAGE (after custom_app unzip) │ +│ │ +│ /Documents/app/question_types/x-ranking/index.js │ +│ /Documents/app/question_types/x-dynamicEnum/index.js │ +│ /Documents/app/question_types/x-custom-text/index.js │ +└────────────────────────┬────────────────────────────────────┘ + │ RNFS.readFile() → string + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ FORMULUS RN — CustomQuestionTypeScanner │ +│ │ +│ 1. Reads each index.js as a raw string │ +│ 2. Screens against blocklist (fetch, eval, etc.) │ +│ 3. Builds manifest with source strings: │ +│ { "x-ranking": { source: "..." } } │ +│ 4. Rejected modules → logged as warnings │ +└────────────────────────┬────────────────────────────────────┘ + │ FormInitData.customQuestionTypes + │ sendFormInit() → injectJavaScript() + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ FORMPLAYER WEBVIEW — App.tsx │ +│ │ +│ initializeForm() reads initData.customQuestionTypes │ +│ calls loadCustomQuestionTypes(manifest) │ +└────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ CustomQuestionTypeLoader.ts — SANDBOX │ +│ │ +│ For each entry in manifest.custom_types: │ +│ new Function('module','exports','React','MaterialUI', │ +│ source) │ +│ Extracts module.exports.default (React component) │ +│ Validates it's a function │ +│ ❌ No access to: fetch, document, localStorage, etc. │ +└────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ CustomQuestionTypeRegistry.ts │ +│ │ +│ For each loaded component: │ +│ Auto-generates a tester: │ +│ rankWith(6, schemaMatches(s => s.format === name)) │ +│ Creates renderer entry via adapter │ +└────────────────────────┬────────────────────────────────────┘ + │ + ┌──────────┴──────────┐ + ▼ ▼ +┌──────────────────────┐ ┌──────────────────────────────────┐ +│ AJV Registration │ │ JsonForms Renderers Array │ +│ │ │ │ +│ ajv.addFormat( │ │ [ │ +│ 'x-ranking', │ │ ...builtInRenderers, │ +│ () => true │ │ ...customTypeRenderers, ← NEW │ +│ ) │ │ ] │ +│ │ │ │ +│ Prevents AJV from │ │ Testers run top-to-bottom, │ +│ rejecting unknown │ │ highest rank wins │ +│ format strings │ │ │ +└──────────────────────┘ └───────────────┬──────────────────┘ + │ at render time + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ CustomQuestionTypeAdapter.tsx │ +│ │ +│ JSON Forms ControlProps → CustomQuestionTypeProps │ +│ ───────────────────── ──────────────────────── │ +│ data value │ +│ handleChange(path, val) onChange(val) │ +│ errors (string) validation { error, msg } │ +│ schema['x-config'] config │ +│ enabled enabled │ +│ path fieldPath │ +│ label, description label, description │ +│ │ +│ Wraps in: QuestionShell + ErrorBoundary │ +└────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Author's Component │ +│ │ +│ Receives only: { value, config, onChange, validation, ... }│ +│ No JSON Forms knowledge needed. │ +│ Crash-safe via ErrorBoundary. │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Schema Examples (based on AnthroCollect forms) + +### Ranking Question + +Used in `p_focal` — pairwise Elo ranking of people by social attributes: + +```json +{ + "ranking_result": { + "type": "object", + "format": "x-ranking", + "x-config": { + "sexFilter": "female", + "hardLimit": 250 + } + } +} +``` + +### Dynamic Enum Question + +Used across many forms — dropdown choices populated from database queries: + +```json +{ + "selected_person": { + "type": "string", + "format": "x-dynamicEnum", + "x-config": { + "query": "p_consent", + "params": { + "scope": "{{data.scope}}" + }, + "valueField": "observationId", + "labelField": "data.name" + } + } +} +``` + +### Custom Text Question + +Enhanced text input with configurable multiline and placeholder: + +```json +{ + "notes": { + "type": "string", + "format": "x-custom-text", + "maxLength": 500, + "x-config": { + "placeholder": "Enter field notes...", + "helperText": "Describe any notable observations" + } + } +} +``` + +**What happens for each:** + +1. `format: "x-ranking"` → tester matches → the ranking renderer is used +2. `x-config` → passed as `props.config` to the author's component +3. Standard JSON Schema keywords (`type`, `maxLength`, etc.) → validated by AJV as normal +4. AJV doesn't reject the custom format strings because we registered them + +--- + +## Implementation Plan (completed) + +All changes below have been implemented. + +### Formulus RN Side + +| File | Change | +|------|--------| +| `FormulusInterfaceDefinition.ts` | `modulePath` → `source` in `FormInitData.customQuestionTypes` | +| `CustomQuestionTypeScanner.ts` | **NEW** — scans, reads, screens question type modules | +| `FormplayerModal.tsx` | Calls scanner, passes source strings in `FormInitData` | + +### FormPlayer WebView Side + +| File | Change | +|------|--------| +| `FormulusInterfaceDefinition.ts` | `modulePath` → `source` (mirror) | +| `CustomQuestionTypeContract.ts` | `modulePath` → `source` in `CustomQuestionTypeManifest` | +| `CustomQuestionTypeLoader.ts` | Rewritten: `import()` → `new Function()` sandbox | + +### Key Files — Full Reference + +| File | Role | Key Export | +|------|------|-----------| +| `CustomQuestionTypeScanner.ts` (RN) | Reads & screens modules | `scanCustomQuestionTypes()` | +| `CustomQuestionTypeContract.ts` | Defines what authors receive | `CustomQuestionTypeProps` | +| `CustomQuestionTypeLoader.ts` | Sandboxed evaluation | `loadCustomQuestionTypes()` | +| `CustomQuestionTypeRegistry.ts` | Creates JsonForms entries | `registerCustomQuestionTypes()` | +| `CustomQuestionTypeAdapter.tsx` | Props bridge + error isolation | `createCustomQuestionTypeRenderer()` | +| `FormulusInterfaceDefinition.ts` | Carries source from RN → WebView | `FormInitData` | +| `App.tsx` | Orchestrates load → register → render | `initializeForm()` | +| `FormplayerModal.tsx` (RN) | Builds FormInitData, sends to WebView | `initializeForm()` | diff --git a/formulus/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx index 98ea8fbba..cc09969eb 100644 --- a/formulus/src/components/FormplayerModal.tsx +++ b/formulus/src/components/FormplayerModal.tsx @@ -35,6 +35,7 @@ import { databaseService } from '../database'; import { colors } from '../theme/colors'; import { FormSpec } from '../services'; // FormService will be imported directly import { ExtensionService } from '../services/ExtensionService'; +import { scanCustomQuestionTypes } from '../services/CustomQuestionTypeScanner'; import RNFS from 'react-native-fs'; import { useAppTheme } from '../contexts/AppThemeContext'; import { geolocationService } from '../services/GeolocationService'; @@ -313,71 +314,19 @@ const FormplayerModal = forwardRef( return; } - // Scan custom question types and read their source code - // Check both root forms/ and app/forms/ paths (same dual-path as FormService) + // Scan custom question types (reads JS files, screens against blocklist) let customQuestionTypes = undefined; try { - const qtDirs = [ - RNFS.DocumentDirectoryPath + '/forms/question_types', - `${customAppPath}/forms/question_types`, - ]; - console.log( - `🔍🔍🔍 [FormplayerModal] Scanning custom question types in: ${qtDirs.join(', ')}`, - ); - - const custom_types: Record = {}; - - for (const qtDir of qtDirs) { - const qtDirExists = await RNFS.exists(qtDir); - if (!qtDirExists) { - console.log( - `🔍 [FormplayerModal] Path not found, skipping: ${qtDir}`, - ); - continue; - } - - const folders = await RNFS.readDir(qtDir); - console.log( - `🔍 [FormplayerModal] Found ${folders.length} items in ${qtDir}: ${folders.map(f => f.name).join(', ')}`, - ); - - for (const folder of folders) { - if (folder.isDirectory() && !custom_types[folder.name]) { - // Try renderer.js first, then index.js as fallback - const rendererPath = `${folder.path}/renderer.js`; - const indexPath = `${folder.path}/index.js`; - const hasRenderer = await RNFS.exists(rendererPath); - const hasIndex = !hasRenderer && (await RNFS.exists(indexPath)); - const jsPath = hasRenderer - ? rendererPath - : hasIndex - ? indexPath - : null; - - if (jsPath) { - // Read the source code so the WebView can evaluate it directly - const source = await RNFS.readFile(jsPath, 'utf8'); - custom_types[folder.name] = { source }; - console.log( - `[FormplayerModal] Custom question type: "${folder.name}" (${source.length} bytes from ${jsPath})`, - ); - } else { - console.warn( - `⚠️ [FormplayerModal] Skipping "${folder.name}": no renderer.js or index.js found`, - ); - } - } - } + const scanResult = await scanCustomQuestionTypes(customAppPath); + if (Object.keys(scanResult.custom_types).length > 0) { + customQuestionTypes = { + custom_types: scanResult.custom_types, + }; } - - if (Object.keys(custom_types).length > 0) { - customQuestionTypes = { custom_types }; - console.log( - `📦📦📦 [FormplayerModal] Custom question types manifest: ${JSON.stringify(Object.keys(custom_types))}`, - ); - } else { + if (scanResult.errors.length > 0) { console.warn( - '⚠️ [FormplayerModal] No custom question types found in any path', + 'Some custom question types failed screening:', + scanResult.errors, ); } } catch (error) { From 7f5fcc9ae3a832ce5b354f5cab6f831798993ca5 Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Thu, 19 Feb 2026 15:46:37 +0300 Subject: [PATCH 18/25] feat: enhance module resolution and improve custom question type handling --- formulus/src/services/FormService.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/formulus/src/services/FormService.ts b/formulus/src/services/FormService.ts index 32d356edf..e6294eb1e 100644 --- a/formulus/src/services/FormService.ts +++ b/formulus/src/services/FormService.ts @@ -115,12 +115,19 @@ export class FormService { } const formSpecFolders = await RNFS.readDir(formSpecsDir); +<<<<<<< HEAD // Skip non-form directories (e.g. extensions/, question_types/, .hidden, temp_*) +======= + // Skip non-form directories (e.g. extensions/, question_types/, .hidden) +>>>>>>> ea89e8f (feat: enhance module resolution and improve custom question type handling) const formDirs = formSpecFolders.filter( f => f.isDirectory() && !f.name.startsWith('.') && +<<<<<<< HEAD !f.name.startsWith('temp_') && +======= +>>>>>>> ea89e8f (feat: enhance module resolution and improve custom question type handling) f.name !== 'extensions' && f.name !== 'question_types', ); From b9e3f8b19ba930d439ef332c857d3c9bcc9a5855 Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Thu, 19 Feb 2026 16:19:40 +0300 Subject: [PATCH 19/25] chore: apply prettier formatting fixes --- formulus-formplayer/src/index.tsx | 6 ++++++ .../src/services/CustomQuestionTypeLoader.ts | 2 +- formulus/src/services/FormService.ts | 7 ------- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/formulus-formplayer/src/index.tsx b/formulus-formplayer/src/index.tsx index fa36a8731..b20543528 100644 --- a/formulus-formplayer/src/index.tsx +++ b/formulus-formplayer/src/index.tsx @@ -9,12 +9,18 @@ import App from './App'; if (typeof window !== 'undefined') { (window as any).React = React; (window as any).MaterialUI = MUI; +<<<<<<< HEAD // Only log in development mode if (import.meta.env.DEV || process.env.NODE_ENV === 'development') { console.log( '[index] Exposed React and MaterialUI to global scope for custom renderers', ); } +======= + console.log( + '[index] Exposed React and MaterialUI to global scope for custom renderers', + ); +>>>>>>> 25a737e (chore: apply prettier formatting fixes) } const root = ReactDOM.createRoot( diff --git a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts index fccca8007..4262b5f2e 100644 --- a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts +++ b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts @@ -178,7 +178,7 @@ export async function loadCustomQuestionTypes( if (result.errors.length > 0) { console.warn( `[CustomQuestionTypeLoader] ${result.errors.length} format(s) failed to load:`, - result.errors.map((e) => e.format).join(', '), + result.errors.map(e => e.format).join(', '), ); } diff --git a/formulus/src/services/FormService.ts b/formulus/src/services/FormService.ts index e6294eb1e..32d356edf 100644 --- a/formulus/src/services/FormService.ts +++ b/formulus/src/services/FormService.ts @@ -115,19 +115,12 @@ export class FormService { } const formSpecFolders = await RNFS.readDir(formSpecsDir); -<<<<<<< HEAD // Skip non-form directories (e.g. extensions/, question_types/, .hidden, temp_*) -======= - // Skip non-form directories (e.g. extensions/, question_types/, .hidden) ->>>>>>> ea89e8f (feat: enhance module resolution and improve custom question type handling) const formDirs = formSpecFolders.filter( f => f.isDirectory() && !f.name.startsWith('.') && -<<<<<<< HEAD !f.name.startsWith('temp_') && -======= ->>>>>>> ea89e8f (feat: enhance module resolution and improve custom question type handling) f.name !== 'extensions' && f.name !== 'question_types', ); From 1166f6ddc39e9768fb476b3021f10af03f53d00e Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Thu, 26 Feb 2026 10:46:07 +0300 Subject: [PATCH 20/25] feat: clean up - Added a private method to recursively remove directories and their contents, handling nested directories and permission issues. - Updated the app bundle extraction process to use a unique staging path, improving conflict management. - Enhanced error handling for directory creation and extraction, ensuring non-fatal errors are logged without interrupting the process. - Increased the timeout for form initialization to reduce false positives in loading status. --- formulus-formplayer/src/index.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/formulus-formplayer/src/index.tsx b/formulus-formplayer/src/index.tsx index b20543528..fa36a8731 100644 --- a/formulus-formplayer/src/index.tsx +++ b/formulus-formplayer/src/index.tsx @@ -9,18 +9,12 @@ import App from './App'; if (typeof window !== 'undefined') { (window as any).React = React; (window as any).MaterialUI = MUI; -<<<<<<< HEAD // Only log in development mode if (import.meta.env.DEV || process.env.NODE_ENV === 'development') { console.log( '[index] Exposed React and MaterialUI to global scope for custom renderers', ); } -======= - console.log( - '[index] Exposed React and MaterialUI to global scope for custom renderers', - ); ->>>>>>> 25a737e (chore: apply prettier formatting fixes) } const root = ReactDOM.createRoot( From cbec829dcea8b452a583535f0da21db2a2b37724 Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Thu, 26 Feb 2026 16:53:26 +0300 Subject: [PATCH 21/25] feat: expose formulus API to custom question type renderers - Add formulus API parameter to sandbox evaluation - Custom renderers can now safely query database via formulus.getObservationsByQuery() - Still maintains sandbox security (blocks fetch, localStorage, etc.) - Add documentation for security options --- CUSTOM_QUESTION_TYPES_SECURITY_OPTIONS.md | 250 ++++++++++++++++++ .../src/services/CustomQuestionTypeLoader.ts | 16 +- 2 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 CUSTOM_QUESTION_TYPES_SECURITY_OPTIONS.md diff --git a/CUSTOM_QUESTION_TYPES_SECURITY_OPTIONS.md b/CUSTOM_QUESTION_TYPES_SECURITY_OPTIONS.md new file mode 100644 index 000000000..7eca7ebfd --- /dev/null +++ b/CUSTOM_QUESTION_TYPES_SECURITY_OPTIONS.md @@ -0,0 +1,250 @@ +# Custom Question Types - Security Options + +This document outlines three approaches to security for custom question types, from most secure to least secure. + +## Current Status + +✅ **Option 1 is now implemented** - Custom renderers can access `window.formulus` API for safe database queries while still being sandboxed from dangerous browser APIs. + +--- + +## Option 1: Sandboxed with Formulus API Access (✅ CURRENT - RECOMMENDED) + +**Security Level:** High +**Ease of Use:** Medium-High + +### What's Allowed: +- ✅ React and MaterialUI (injected) +- ✅ `window.formulus` API (for database queries) +- ✅ Standard JavaScript (arrays, objects, functions, etc.) + +### What's Blocked: +- ❌ `fetch()`, `XMLHttpRequest`, `WebSocket` (network requests) +- ❌ `localStorage`, `sessionStorage`, `indexedDB` (storage) +- ❌ `document.cookie`, `navigator.sendBeacon` (data exfiltration) +- ❌ `eval()`, `new Function()` (code injection) +- ❌ Direct DOM manipulation + +### How Custom Renderers Access Data: + +```javascript +function MyRenderer({ value, onChange, config }) { + const [people, setPeople] = React.useState([]); + + React.useEffect(() => { + // Safe database query via Formulus API + if (formulus?.getObservationsByQuery) { + formulus.getObservationsByQuery({ + formType: 'person', + whereClause: "data.sex = 'male'" + }).then(observations => { + setPeople(observations.map(obs => obs.data)); + }); + } + }, []); + + // Render UI using React.createElement and MaterialUI + return React.createElement(MaterialUI.Box, {}, /* ... */); +} + +module.exports = { default: MyRenderer }; +``` + +### Benefits: +- ✅ Safe: No network requests or data exfiltration possible +- ✅ Flexible: Can query database via controlled API +- ✅ Isolated: Errors in custom code don't crash the app +- ✅ Maintainable: Clear security boundaries + +### Drawbacks: +- ⚠️ Must use `React.createElement()` instead of JSX +- ⚠️ Must use `formulus` API instead of direct fetch + +--- + +## Option 2: Remove Sandbox (Full Browser Access) + +**Security Level:** Low +**Ease of Use:** Very High + +### What Would Change: + +1. **Remove sandbox evaluation** - Use standard `eval()` or dynamic import +2. **Remove static blocklist** - Allow all JavaScript patterns +3. **Full browser API access** - `fetch`, `localStorage`, `document`, etc. + +### Implementation: + +**In `CustomQuestionTypeLoader.ts`:** + +```typescript +// REPLACE the sandboxed evaluation with: +function evaluateModuleInSandbox( + source: string, + formatName: string, +): React.ComponentType { + // Option 2A: Use eval() directly (full access) + const exports: Record = {}; + const moduleObj = { exports }; + + // Wrap in IIFE to avoid polluting global scope + const wrappedSource = ` + (function(module, exports) { + ${source} + })(module, exports); + `; + + eval(wrappedSource); + + // OR Option 2B: Use dynamic import (requires file path) + // This would require changing the architecture to pass file paths + // instead of source strings + + const component = moduleObj.exports.default ?? moduleObj.exports; + if (typeof component !== 'function') { + throw new Error(`Module "${formatName}" does not export a valid React component`); + } + return component as React.ComponentType; +} +``` + +**In `CustomQuestionTypeScanner.ts`:** + +```typescript +// REMOVE or comment out the blocklist: +const BLOCKED_PATTERNS: Array<{ pattern: RegExp; description: string }> = [ + // Comment out all patterns to allow everything +]; +``` + +### How Custom Renderers Would Work: + +```javascript +// Now they can use JSX, fetch, localStorage, etc. +function MyRenderer({ value, onChange, config }) { + const [data, setData] = React.useState(null); + + React.useEffect(() => { + // Direct fetch - now allowed! + fetch('/api/people') + .then(res => res.json()) + .then(setData); + }, []); + + // Can use JSX if you add a JSX transform + return ( + + {data?.name} + + ); +} + +module.exports = { default: MyRenderer }; +``` + +### Benefits: +- ✅ Very easy to write (can use JSX, fetch, standard imports) +- ✅ Full JavaScript ecosystem available +- ✅ Can use any npm package (if bundled) + +### Drawbacks: +- ❌ **Security Risk**: Malicious code can: + - Make unauthorized network requests + - Access and exfiltrate localStorage data + - Manipulate the DOM + - Access cookies and session data + - Run arbitrary code via `eval()` +- ❌ **No Isolation**: Bugs in custom code can crash the entire form +- ❌ **Trust Required**: Must trust all app bundle authors completely + +### When to Use: +- Only if you have complete control over app bundle sources +- Only if all form developers are trusted +- Not recommended for production with untrusted sources + +--- + +## Option 3: Hybrid Approach (Selective Whitelist) + +**Security Level:** Medium +**Ease of Use:** High + +### What Would Change: + +Allow specific safe APIs while blocking dangerous ones: + +```typescript +// In CustomQuestionTypeLoader.ts +const factory = new Function( + 'module', + 'exports', + 'React', + 'MaterialUI', + 'formulus', + 'console', // Allow console for debugging + 'setTimeout', // Allow async operations + 'setInterval', + 'clearTimeout', + 'clearInterval', + 'Promise', // Allow promises + 'JSON', // Allow JSON parsing + source, +); + +factory( + moduleObj, + exports, + ReactLib, + MUILib, + FormulusAPI, + console, + setTimeout, + setInterval, + clearTimeout, + clearInterval, + Promise, + JSON +); +``` + +### Benefits: +- ✅ More APIs available (console, timers, JSON) +- ✅ Still blocks dangerous APIs (fetch, localStorage) +- ✅ Better developer experience + +### Drawbacks: +- ⚠️ More complex to maintain +- ⚠️ Need to carefully vet each API + +--- + +## Recommendation + +**Use Option 1 (Current Implementation)** because: + +1. **Security**: Prevents data exfiltration and unauthorized network requests +2. **Flexibility**: `window.formulus` API provides all necessary data access +3. **Isolation**: Errors in custom code don't crash the app +4. **Maintainability**: Clear security boundaries + +If you need JSX support, consider adding a lightweight JSX transform instead of removing the sandbox. + +--- + +## Migration Path + +If you want to move from Option 1 to Option 2: + +1. Update `CustomQuestionTypeLoader.ts` to use `eval()` instead of sandboxed `new Function()` +2. Remove or disable the blocklist in `CustomQuestionTypeScanner.ts` +3. Update documentation to warn about security implications +4. Consider adding code signing or other trust mechanisms + +--- + +## Questions? + +- **"Can custom renderers access the database?"** → Yes, via `window.formulus.getObservationsByQuery()` (Option 1) +- **"Can they make HTTP requests?"** → No in Option 1, Yes in Option 2 (security risk) +- **"Can they use JSX?"** → Not directly, but you can add a JSX transform +- **"Can they use npm packages?"** → Only if pre-bundled in the app bundle diff --git a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts index 4262b5f2e..91bc91762 100644 --- a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts +++ b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts @@ -40,12 +40,14 @@ export interface CustomQuestionTypeLoadResult { /** * Evaluate a module source string in a scoped sandbox. * - * The code only has access to the variables we explicitly pass in: + * The code has access to the variables we explicitly pass in: * - module / exports (CommonJS-style export mechanism) * - React (so the component can use createElement, hooks, etc.) + * - MaterialUI (full @mui/material package) + * - formulus (window.formulus API for safe database queries via getObservationsByQuery) * * Dangerous globals (fetch, XMLHttpRequest, document, localStorage, etc.) - * are NOT available in this scope. + * are NOT available in this scope, but formulus API provides safe data access. */ function evaluateModuleInSandbox( source: string, @@ -78,19 +80,25 @@ function evaluateModuleInSandbox( (globalThis as unknown as Record).MaterialUI || (self as unknown as Record).MaterialUI; + // Get Formulus API from global scope (for safe database queries) + const FormulusAPI = + (window as unknown as Record).formulus || null; + try { // Create a factory function with a restricted scope. - // The code can only access: module, exports, React, MaterialUI + // The code can access: module, exports, React, MaterialUI, formulus (for database queries) // It CANNOT access: fetch, XMLHttpRequest, document, localStorage, etc. + // formulus API provides safe access to getObservationsByQuery, etc. const factory = new Function( 'module', 'exports', 'React', 'MaterialUI', + 'formulus', source, ); - factory(moduleObj, exports, ReactLib, MUILib); + factory(moduleObj, exports, ReactLib, MUILib, FormulusAPI); } catch (err) { throw new Error( `Failed to evaluate module source: ${err instanceof Error ? err.message : String(err)}`, From 851e32cc44c3706021f4a097fa0e7de410ac000a Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Mon, 2 Mar 2026 09:11:12 +0300 Subject: [PATCH 22/25] fix: correct custom question type scanning path and clean up - Fix scanning path to check app/question_types (was app/forms/question_types) - Add fallback to get people data from root schema for ranking format - Remove CustomQuestionTypeScanner.ts (use inline scanning per PR #333) - Remove security blocklist and documentation files - Revert bundle extraction fixes (handled in AnthroCollect) - Remove x-config references - Align with PR #333 style (inline evaluation) - Remove debug logging and instrumentation --- CUSTOM_QUESTION_TYPES_SECURITY_OPTIONS.md | 250 ----------- .../custom-question-types-architecture.md | 404 ------------------ .../renderers/CustomQuestionTypeAdapter.tsx | 26 +- .../src/services/CustomQuestionTypeLoader.ts | 142 ++---- .../src/types/CustomQuestionTypeContract.ts | 12 +- formulus/src/api/synkronus/index.ts | 159 +------ formulus/src/components/FormplayerModal.tsx | 60 ++- .../src/services/CustomQuestionTypeScanner.ts | 193 --------- 8 files changed, 135 insertions(+), 1111 deletions(-) delete mode 100644 CUSTOM_QUESTION_TYPES_SECURITY_OPTIONS.md delete mode 100644 formulus-formplayer/docs/custom-question-types-architecture.md delete mode 100644 formulus/src/services/CustomQuestionTypeScanner.ts diff --git a/CUSTOM_QUESTION_TYPES_SECURITY_OPTIONS.md b/CUSTOM_QUESTION_TYPES_SECURITY_OPTIONS.md deleted file mode 100644 index 7eca7ebfd..000000000 --- a/CUSTOM_QUESTION_TYPES_SECURITY_OPTIONS.md +++ /dev/null @@ -1,250 +0,0 @@ -# Custom Question Types - Security Options - -This document outlines three approaches to security for custom question types, from most secure to least secure. - -## Current Status - -✅ **Option 1 is now implemented** - Custom renderers can access `window.formulus` API for safe database queries while still being sandboxed from dangerous browser APIs. - ---- - -## Option 1: Sandboxed with Formulus API Access (✅ CURRENT - RECOMMENDED) - -**Security Level:** High -**Ease of Use:** Medium-High - -### What's Allowed: -- ✅ React and MaterialUI (injected) -- ✅ `window.formulus` API (for database queries) -- ✅ Standard JavaScript (arrays, objects, functions, etc.) - -### What's Blocked: -- ❌ `fetch()`, `XMLHttpRequest`, `WebSocket` (network requests) -- ❌ `localStorage`, `sessionStorage`, `indexedDB` (storage) -- ❌ `document.cookie`, `navigator.sendBeacon` (data exfiltration) -- ❌ `eval()`, `new Function()` (code injection) -- ❌ Direct DOM manipulation - -### How Custom Renderers Access Data: - -```javascript -function MyRenderer({ value, onChange, config }) { - const [people, setPeople] = React.useState([]); - - React.useEffect(() => { - // Safe database query via Formulus API - if (formulus?.getObservationsByQuery) { - formulus.getObservationsByQuery({ - formType: 'person', - whereClause: "data.sex = 'male'" - }).then(observations => { - setPeople(observations.map(obs => obs.data)); - }); - } - }, []); - - // Render UI using React.createElement and MaterialUI - return React.createElement(MaterialUI.Box, {}, /* ... */); -} - -module.exports = { default: MyRenderer }; -``` - -### Benefits: -- ✅ Safe: No network requests or data exfiltration possible -- ✅ Flexible: Can query database via controlled API -- ✅ Isolated: Errors in custom code don't crash the app -- ✅ Maintainable: Clear security boundaries - -### Drawbacks: -- ⚠️ Must use `React.createElement()` instead of JSX -- ⚠️ Must use `formulus` API instead of direct fetch - ---- - -## Option 2: Remove Sandbox (Full Browser Access) - -**Security Level:** Low -**Ease of Use:** Very High - -### What Would Change: - -1. **Remove sandbox evaluation** - Use standard `eval()` or dynamic import -2. **Remove static blocklist** - Allow all JavaScript patterns -3. **Full browser API access** - `fetch`, `localStorage`, `document`, etc. - -### Implementation: - -**In `CustomQuestionTypeLoader.ts`:** - -```typescript -// REPLACE the sandboxed evaluation with: -function evaluateModuleInSandbox( - source: string, - formatName: string, -): React.ComponentType { - // Option 2A: Use eval() directly (full access) - const exports: Record = {}; - const moduleObj = { exports }; - - // Wrap in IIFE to avoid polluting global scope - const wrappedSource = ` - (function(module, exports) { - ${source} - })(module, exports); - `; - - eval(wrappedSource); - - // OR Option 2B: Use dynamic import (requires file path) - // This would require changing the architecture to pass file paths - // instead of source strings - - const component = moduleObj.exports.default ?? moduleObj.exports; - if (typeof component !== 'function') { - throw new Error(`Module "${formatName}" does not export a valid React component`); - } - return component as React.ComponentType; -} -``` - -**In `CustomQuestionTypeScanner.ts`:** - -```typescript -// REMOVE or comment out the blocklist: -const BLOCKED_PATTERNS: Array<{ pattern: RegExp; description: string }> = [ - // Comment out all patterns to allow everything -]; -``` - -### How Custom Renderers Would Work: - -```javascript -// Now they can use JSX, fetch, localStorage, etc. -function MyRenderer({ value, onChange, config }) { - const [data, setData] = React.useState(null); - - React.useEffect(() => { - // Direct fetch - now allowed! - fetch('/api/people') - .then(res => res.json()) - .then(setData); - }, []); - - // Can use JSX if you add a JSX transform - return ( - - {data?.name} - - ); -} - -module.exports = { default: MyRenderer }; -``` - -### Benefits: -- ✅ Very easy to write (can use JSX, fetch, standard imports) -- ✅ Full JavaScript ecosystem available -- ✅ Can use any npm package (if bundled) - -### Drawbacks: -- ❌ **Security Risk**: Malicious code can: - - Make unauthorized network requests - - Access and exfiltrate localStorage data - - Manipulate the DOM - - Access cookies and session data - - Run arbitrary code via `eval()` -- ❌ **No Isolation**: Bugs in custom code can crash the entire form -- ❌ **Trust Required**: Must trust all app bundle authors completely - -### When to Use: -- Only if you have complete control over app bundle sources -- Only if all form developers are trusted -- Not recommended for production with untrusted sources - ---- - -## Option 3: Hybrid Approach (Selective Whitelist) - -**Security Level:** Medium -**Ease of Use:** High - -### What Would Change: - -Allow specific safe APIs while blocking dangerous ones: - -```typescript -// In CustomQuestionTypeLoader.ts -const factory = new Function( - 'module', - 'exports', - 'React', - 'MaterialUI', - 'formulus', - 'console', // Allow console for debugging - 'setTimeout', // Allow async operations - 'setInterval', - 'clearTimeout', - 'clearInterval', - 'Promise', // Allow promises - 'JSON', // Allow JSON parsing - source, -); - -factory( - moduleObj, - exports, - ReactLib, - MUILib, - FormulusAPI, - console, - setTimeout, - setInterval, - clearTimeout, - clearInterval, - Promise, - JSON -); -``` - -### Benefits: -- ✅ More APIs available (console, timers, JSON) -- ✅ Still blocks dangerous APIs (fetch, localStorage) -- ✅ Better developer experience - -### Drawbacks: -- ⚠️ More complex to maintain -- ⚠️ Need to carefully vet each API - ---- - -## Recommendation - -**Use Option 1 (Current Implementation)** because: - -1. **Security**: Prevents data exfiltration and unauthorized network requests -2. **Flexibility**: `window.formulus` API provides all necessary data access -3. **Isolation**: Errors in custom code don't crash the app -4. **Maintainability**: Clear security boundaries - -If you need JSX support, consider adding a lightweight JSX transform instead of removing the sandbox. - ---- - -## Migration Path - -If you want to move from Option 1 to Option 2: - -1. Update `CustomQuestionTypeLoader.ts` to use `eval()` instead of sandboxed `new Function()` -2. Remove or disable the blocklist in `CustomQuestionTypeScanner.ts` -3. Update documentation to warn about security implications -4. Consider adding code signing or other trust mechanisms - ---- - -## Questions? - -- **"Can custom renderers access the database?"** → Yes, via `window.formulus.getObservationsByQuery()` (Option 1) -- **"Can they make HTTP requests?"** → No in Option 1, Yes in Option 2 (security risk) -- **"Can they use JSX?"** → Not directly, but you can add a JSX transform -- **"Can they use npm packages?"** → Only if pre-bundled in the app bundle diff --git a/formulus-formplayer/docs/custom-question-types-architecture.md b/formulus-formplayer/docs/custom-question-types-architecture.md deleted file mode 100644 index bb53d1cab..000000000 --- a/formulus-formplayer/docs/custom-question-types-architecture.md +++ /dev/null @@ -1,404 +0,0 @@ -# Custom Question Types — Architecture & Flow - ---- - -## File Structure - -``` -formulus-formplayer/src/ (FORMPLAYER — runs in WebView) -├── types/ -│ ├── CustomQuestionTypeContract.ts ← 1. The contract authors code against -│ └── FormulusInterfaceDefinition.ts ← 2. FormInitData (carries the manifest) -│ -├── services/ -│ ├── CustomQuestionTypeLoader.ts ← 3. Sandboxed evaluation of source strings -│ └── CustomQuestionTypeRegistry.ts ← 4. Auto-generates testers + renderer entries -│ -├── renderers/ -│ └── CustomQuestionTypeAdapter.tsx ← 5. Bridges ControlProps → CustomQuestionTypeProps -│ -└── App.tsx ← 6. Orchestrates everything - -formulus/src/ (FORMULUS — runs in React Native) -├── services/ -│ └── CustomQuestionTypeScanner.ts ← Reads files, screens against blocklist -│ -└── components/ - └── FormplayerModal.tsx ← Calls scanner, passes source in FormInitData -``` - -### Author's Side (custom_app) - -``` -custom_app/ -└── question_types/ - ├── x-ranking/ - │ └── index.js ← default export: React component - ├── x-dynamicEnum/ - │ └── index.js - └── x-custom-text/ - └── index.js -``` - ---- - -## Security Model — Source Extraction - -Custom question type JS files could contain malicious code. Instead of letting the WebView -`import()` arbitrary scripts (which would give them full access to fetch, DOM, localStorage, etc.), -we use a **source extraction** approach with two layers of defense: - -| Layer | Where | What it does | -|-------|-------|-------------| -| **Static blocklist** | RN side (`CustomQuestionTypeScanner`) | Rejects code containing dangerous patterns before it reaches the WebView | -| **Scoped evaluation** | WebView (`CustomQuestionTypeLoader`) | `new Function()` sandbox — code can only access React and MUI, nothing else | - -### Blocked Patterns (RN-side screening) - -``` -fetch( — network requests -XMLHttpRequest — network requests -WebSocket — persistent connections -eval( — dynamic code execution -new Function( — dynamic code execution -document.cookie — cookie access -localStorage — storage access -sessionStorage — storage access -indexedDB — database access -navigator.sendBeacon — data exfiltration -importScripts( — script injection -``` - -### Scoped Sandbox (WebView-side evaluation) - -```javascript -// Instead of: import("file:///path/to/index.js") -// We do: -const factory = new Function( - 'module', 'exports', 'React', 'MaterialUI', - sourceString // ← sent from RN as a string, not a file path -); - -// Custom code CAN access: React, MaterialUI, module, exports -// Custom code CANNOT access: fetch, document, localStorage, window, etc. -``` - ---- - -## How Module Loading Works - -### 1. Device Storage - -When the custom_app archive is unzipped, files land on the device filesystem: - -``` -/data/.../Documents/app/ -├── forms/ -│ ├── hh_hut/schema.json -│ ├── hh_person/schema.json -│ ├── p_focal/schema.json -│ └── ... -└── question_types/ - ├── x-ranking/index.js ← pairwise Elo ranking UI - ├── x-dynamicEnum/index.js ← dynamic choice list from DB queries - └── x-custom-text/index.js ← enhanced text input -``` - -### 2. Formulus RN Scans, Reads & Screens - -`CustomQuestionTypeScanner.ts` scans `question_types/`, reads each `index.js` as a raw string, -and screens it against the blocklist: - -```typescript -// In CustomQuestionTypeScanner.ts -const questionTypesDir = `${customAppPath}/question_types`; -const folders = await RNFS.readDir(questionTypesDir); - -for (const folder of folders) { - if (folder.isDirectory()) { - const source = await RNFS.readFile(`${folder.path}/index.js`, 'utf8'); - - // Screen against blocklist - const violation = screenSource(source); - if (violation) { - errors.push({ name: folder.name, error: `Blocked: ${violation}` }); - continue; - } - - // Source is clean — include it - custom_types[folder.name] = { source }; - } -} -``` - -**Sample manifest** (source strings, not file paths): - -```json -{ - "custom_types": { - "x-ranking": { - "source": "(function() { 'use strict'; ... module.exports = RankingRenderer; })()" - }, - "x-dynamicEnum": { - "source": "(function() { 'use strict'; ... module.exports = DynamicEnumControl; })()" - }, - "x-custom-text": { - "source": "(function() { 'use strict'; ... module.exports = CustomTextRenderer; })()" - } - } -} -``` - -### 3. FormInitData Carries the Source Strings - -In `FormplayerModal.tsx`, `initializeForm()` calls the scanner and includes the result: - -```typescript -const customAppPath = RNFS.DocumentDirectoryPath + '/app'; - -// Scan and screen custom question types -const scanResult = await scanCustomQuestionTypes(customAppPath); -if (scanResult.errors.length > 0) { - console.warn('Some custom question types failed screening:', scanResult.errors); -} - -const formInitData = { - formType: formType.id, - observationId, - params: formParams, - savedData: existingObservationData || {}, - formSchema: formType.schema, - uiSchema: formType.uiSchema ?? {}, - extensions, - customQuestionTypes: { - custom_types: scanResult.custom_types, - }, -} as FormInitData; -``` - -### 4. WebView Receives & Evaluates in Sandbox - -`FormulusWebViewHandler.sendFormInit()` serializes the `FormInitData` and injects it into -the WebView. Then `CustomQuestionTypeLoader.ts` evaluates each source in a scoped sandbox: - -```typescript -// CustomQuestionTypeLoader.ts — evaluateModuleInSandbox() -const exports = {}; -const moduleObj = { exports }; - -const factory = new Function( - 'module', 'exports', 'React', 'MaterialUI', - meta.source, -); - -factory(moduleObj, exports, React, MaterialUI); - -// Extract only the component -const component = moduleObj.exports.default ?? moduleObj.exports; -``` - -### 5. Registry & Rendering - -`CustomQuestionTypeRegistry.ts` takes each loaded component and: -- Auto-generates a tester: `rankWith(6, schemaMatches(s => s.format === name))` -- Creates a renderer entry via `CustomQuestionTypeAdapter.tsx` -- Registers the format with AJV: `ajv.addFormat('x-ranking', () => true)` - ---- - -## Flow Diagram - -``` -┌─────────────────────────────────────────────────────────────┐ -│ DEVICE STORAGE (after custom_app unzip) │ -│ │ -│ /Documents/app/question_types/x-ranking/index.js │ -│ /Documents/app/question_types/x-dynamicEnum/index.js │ -│ /Documents/app/question_types/x-custom-text/index.js │ -└────────────────────────┬────────────────────────────────────┘ - │ RNFS.readFile() → string - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ FORMULUS RN — CustomQuestionTypeScanner │ -│ │ -│ 1. Reads each index.js as a raw string │ -│ 2. Screens against blocklist (fetch, eval, etc.) │ -│ 3. Builds manifest with source strings: │ -│ { "x-ranking": { source: "..." } } │ -│ 4. Rejected modules → logged as warnings │ -└────────────────────────┬────────────────────────────────────┘ - │ FormInitData.customQuestionTypes - │ sendFormInit() → injectJavaScript() - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ FORMPLAYER WEBVIEW — App.tsx │ -│ │ -│ initializeForm() reads initData.customQuestionTypes │ -│ calls loadCustomQuestionTypes(manifest) │ -└────────────────────────┬────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ CustomQuestionTypeLoader.ts — SANDBOX │ -│ │ -│ For each entry in manifest.custom_types: │ -│ new Function('module','exports','React','MaterialUI', │ -│ source) │ -│ Extracts module.exports.default (React component) │ -│ Validates it's a function │ -│ ❌ No access to: fetch, document, localStorage, etc. │ -└────────────────────────┬────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ CustomQuestionTypeRegistry.ts │ -│ │ -│ For each loaded component: │ -│ Auto-generates a tester: │ -│ rankWith(6, schemaMatches(s => s.format === name)) │ -│ Creates renderer entry via adapter │ -└────────────────────────┬────────────────────────────────────┘ - │ - ┌──────────┴──────────┐ - ▼ ▼ -┌──────────────────────┐ ┌──────────────────────────────────┐ -│ AJV Registration │ │ JsonForms Renderers Array │ -│ │ │ │ -│ ajv.addFormat( │ │ [ │ -│ 'x-ranking', │ │ ...builtInRenderers, │ -│ () => true │ │ ...customTypeRenderers, ← NEW │ -│ ) │ │ ] │ -│ │ │ │ -│ Prevents AJV from │ │ Testers run top-to-bottom, │ -│ rejecting unknown │ │ highest rank wins │ -│ format strings │ │ │ -└──────────────────────┘ └───────────────┬──────────────────┘ - │ at render time - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ CustomQuestionTypeAdapter.tsx │ -│ │ -│ JSON Forms ControlProps → CustomQuestionTypeProps │ -│ ───────────────────── ──────────────────────── │ -│ data value │ -│ handleChange(path, val) onChange(val) │ -│ errors (string) validation { error, msg } │ -│ schema['x-config'] config │ -│ enabled enabled │ -│ path fieldPath │ -│ label, description label, description │ -│ │ -│ Wraps in: QuestionShell + ErrorBoundary │ -└────────────────────────┬────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Author's Component │ -│ │ -│ Receives only: { value, config, onChange, validation, ... }│ -│ No JSON Forms knowledge needed. │ -│ Crash-safe via ErrorBoundary. │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Schema Examples (based on AnthroCollect forms) - -### Ranking Question - -Used in `p_focal` — pairwise Elo ranking of people by social attributes: - -```json -{ - "ranking_result": { - "type": "object", - "format": "x-ranking", - "x-config": { - "sexFilter": "female", - "hardLimit": 250 - } - } -} -``` - -### Dynamic Enum Question - -Used across many forms — dropdown choices populated from database queries: - -```json -{ - "selected_person": { - "type": "string", - "format": "x-dynamicEnum", - "x-config": { - "query": "p_consent", - "params": { - "scope": "{{data.scope}}" - }, - "valueField": "observationId", - "labelField": "data.name" - } - } -} -``` - -### Custom Text Question - -Enhanced text input with configurable multiline and placeholder: - -```json -{ - "notes": { - "type": "string", - "format": "x-custom-text", - "maxLength": 500, - "x-config": { - "placeholder": "Enter field notes...", - "helperText": "Describe any notable observations" - } - } -} -``` - -**What happens for each:** - -1. `format: "x-ranking"` → tester matches → the ranking renderer is used -2. `x-config` → passed as `props.config` to the author's component -3. Standard JSON Schema keywords (`type`, `maxLength`, etc.) → validated by AJV as normal -4. AJV doesn't reject the custom format strings because we registered them - ---- - -## Implementation Plan (completed) - -All changes below have been implemented. - -### Formulus RN Side - -| File | Change | -|------|--------| -| `FormulusInterfaceDefinition.ts` | `modulePath` → `source` in `FormInitData.customQuestionTypes` | -| `CustomQuestionTypeScanner.ts` | **NEW** — scans, reads, screens question type modules | -| `FormplayerModal.tsx` | Calls scanner, passes source strings in `FormInitData` | - -### FormPlayer WebView Side - -| File | Change | -|------|--------| -| `FormulusInterfaceDefinition.ts` | `modulePath` → `source` (mirror) | -| `CustomQuestionTypeContract.ts` | `modulePath` → `source` in `CustomQuestionTypeManifest` | -| `CustomQuestionTypeLoader.ts` | Rewritten: `import()` → `new Function()` sandbox | - -### Key Files — Full Reference - -| File | Role | Key Export | -|------|------|-----------| -| `CustomQuestionTypeScanner.ts` (RN) | Reads & screens modules | `scanCustomQuestionTypes()` | -| `CustomQuestionTypeContract.ts` | Defines what authors receive | `CustomQuestionTypeProps` | -| `CustomQuestionTypeLoader.ts` | Sandboxed evaluation | `loadCustomQuestionTypes()` | -| `CustomQuestionTypeRegistry.ts` | Creates JsonForms entries | `registerCustomQuestionTypes()` | -| `CustomQuestionTypeAdapter.tsx` | Props bridge + error isolation | `createCustomQuestionTypeRenderer()` | -| `FormulusInterfaceDefinition.ts` | Carries source from RN → WebView | `FormInitData` | -| `App.tsx` | Orchestrates load → register → render | `initializeForm()` | -| `FormplayerModal.tsx` (RN) | Builds FormInitData, sends to WebView | `initializeForm()` | diff --git a/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx b/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx index 898c04576..bd021669e 100644 --- a/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx +++ b/formulus-formplayer/src/renderers/CustomQuestionTypeAdapter.tsx @@ -7,7 +7,7 @@ */ import React, { Component, type ErrorInfo, type ReactNode } from 'react'; -import { withJsonFormsControlProps } from '@jsonforms/react'; +import { withJsonFormsControlProps, useJsonForms } from '@jsonforms/react'; import type { ControlProps } from '@jsonforms/core'; import QuestionShell from '../components/QuestionShell'; import type { CustomQuestionTypeProps } from '../types/CustomQuestionTypeContract'; @@ -137,6 +137,7 @@ export function createCustomQuestionTypeRenderer( // Extract all schema properties (except reserved ones) as config // This allows parameters alongside "format" to be passed to the renderer const schemaObj = schema as Record; + const RESERVED_PROPERTIES = new Set([ 'type', 'title', @@ -171,12 +172,22 @@ export function createCustomQuestionTypeRenderer( } } - // Merge with x-config (x-config takes precedence for explicit configuration) - const xConfig = schemaObj['x-config'] as - | Record - | undefined; - if (xConfig) { - Object.assign(config, xConfig); + const jsonFormsContext = useJsonForms(); + + // For ranking format: if people not in field schema, try to get from root schema + if (schemaObj.format === 'ranking' && !config.people && jsonFormsContext?.core?.schema) { + const rootSchema = jsonFormsContext.core.schema as Record; + const rootProperties = rootSchema.properties as Record | undefined; + if (rootProperties && path) { + // Extract field name from path (e.g., "#/properties/ranking_field" -> "ranking_field") + const fieldName = path.split('/').pop(); + if (fieldName && rootProperties[fieldName]) { + const fieldSchema = rootProperties[fieldName] as Record; + if (fieldSchema.people) { + config.people = fieldSchema.people; + } + } + } } const customProps: CustomQuestionTypeProps = { @@ -191,6 +202,7 @@ export function createCustomQuestionTypeRenderer( fieldPath: path, label: label ?? '', description: description, + jsonFormsContext, }; return ( diff --git a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts index 91bc91762..2c32e90ad 100644 --- a/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts +++ b/formulus-formplayer/src/services/CustomQuestionTypeLoader.ts @@ -1,19 +1,15 @@ /** * CustomQuestionTypeLoader.ts * - * Loads custom question type components from source strings provided by the - * Formulus RN side. Instead of dynamically importing files from the filesystem, - * this loader evaluates each module's source in a scoped sandbox using - * `new Function()`, which restricts what the code can access. - * - * Security layers: - * 1. RN-side static blocklist (in CustomQuestionTypeScanner) rejects dangerous patterns - * 2. Scoped evaluation here only exposes React — no fetch, document, localStorage, etc. + * Loads custom question type modules from source strings. + * The native Formulus RN side reads each renderer's JS source and + * passes it in the manifest. This loader evaluates each source in + * a CommonJS-compatible sandbox. * * This loader: * 1. Iterates over the manifest - * 2. Evaluates each source string in a scoped sandbox - * 3. Extracts and validates the default export (must be a React component function) + * 2. Evaluates each module's source with CommonJS shims (module, exports) + * 3. Validates the default export is a function (React component) * 4. Passes all loaded components to the registry * 5. Returns renderer entries + format strings for AJV registration * @@ -37,89 +33,6 @@ export interface CustomQuestionTypeLoadResult { errors: Array<{ format: string; error: string }>; } -/** - * Evaluate a module source string in a scoped sandbox. - * - * The code has access to the variables we explicitly pass in: - * - module / exports (CommonJS-style export mechanism) - * - React (so the component can use createElement, hooks, etc.) - * - MaterialUI (full @mui/material package) - * - formulus (window.formulus API for safe database queries via getObservationsByQuery) - * - * Dangerous globals (fetch, XMLHttpRequest, document, localStorage, etc.) - * are NOT available in this scope, but formulus API provides safe data access. - */ -function evaluateModuleInSandbox( - source: string, - formatName: string, -): React.ComponentType { - const exports: Record = {}; - const moduleObj = { exports }; - - // Get React from the global scope (it's available in the WebView) - // Try multiple ways to access it (window, globalThis, self) - const ReactLib = - (window as unknown as Record).React || - (globalThis as unknown as Record).React || - (self as unknown as Record).React; - - if (!ReactLib) { - console.error( - '[CustomQuestionTypeLoader] React not found in window, globalThis, or self', - ); - console.error( - '[CustomQuestionTypeLoader] Available window keys:', - Object.keys(window).slice(0, 20), - ); - throw new Error('React is not available in the global scope'); - } - - // Get MUI from the global scope (custom components may use Material UI) - const MUILib = - (window as unknown as Record).MaterialUI || - (globalThis as unknown as Record).MaterialUI || - (self as unknown as Record).MaterialUI; - - // Get Formulus API from global scope (for safe database queries) - const FormulusAPI = - (window as unknown as Record).formulus || null; - - try { - // Create a factory function with a restricted scope. - // The code can access: module, exports, React, MaterialUI, formulus (for database queries) - // It CANNOT access: fetch, XMLHttpRequest, document, localStorage, etc. - // formulus API provides safe access to getObservationsByQuery, etc. - const factory = new Function( - 'module', - 'exports', - 'React', - 'MaterialUI', - 'formulus', - source, - ); - - factory(moduleObj, exports, ReactLib, MUILib, FormulusAPI); - } catch (err) { - throw new Error( - `Failed to evaluate module source: ${err instanceof Error ? err.message : String(err)}`, - ); - } - - // Extract the component from exports (support both default and module.exports patterns) - const component = - (moduleObj.exports as Record).default ?? moduleObj.exports; - - if (typeof component !== 'function') { - throw new Error( - `Module "${formatName}" does not export a valid React component. ` + - `Expected a function, got ${typeof component}. ` + - `Make sure your module uses module.exports = Component or exports.default = Component.`, - ); - } - - return component as React.ComponentType; -} - /** * Load custom question types from a manifest containing source strings. * @@ -153,13 +66,48 @@ export async function loadCustomQuestionTypes( for (const [formatName, meta] of Object.entries(manifest.custom_types)) { try { console.log( - `[CustomQuestionTypeLoader] Evaluating "${formatName}" (${meta.source.length} bytes)`, + `[CustomQuestionTypeLoader] Loading "${formatName}" (${meta.source.length} bytes)`, ); - // Evaluate the source in a scoped sandbox - const component = evaluateModuleInSandbox(meta.source, formatName); + // Create a CommonJS-compatible sandbox with module/exports shims + // The renderers use: module.exports = { default: ComponentFunction } + // They also expect React and MaterialUI as globals + const moduleShim: { exports: Record } = { + exports: {}, + }; + const exportsShim = moduleShim.exports; + + // Evaluate the source in a function scope with CommonJS shims + const factory = new Function( + 'module', + 'exports', + 'React', + 'MaterialUI', + meta.source, + ); + factory( + moduleShim, + exportsShim, + (window as any).React, + (window as any).MaterialUI, + ); - loadedComponents.set(formatName, component); + // Extract the component: try module.exports.default, then module.exports itself + const component = moduleShim.exports.default ?? moduleShim.exports; + + // Validate that the export is a function (React component) + if (typeof component !== 'function') { + throw new Error( + `Module does not export a valid React component. ` + + `Expected a function, got ${typeof component}. ` + + `Make sure your module exports a default function.`, + ); + } + + loadedComponents.set( + formatName, + component as React.ComponentType, + ); result.formats.push(formatName); console.log( diff --git a/formulus-formplayer/src/types/CustomQuestionTypeContract.ts b/formulus-formplayer/src/types/CustomQuestionTypeContract.ts index 1053d9de7..68a08f10b 100644 --- a/formulus-formplayer/src/types/CustomQuestionTypeContract.ts +++ b/formulus-formplayer/src/types/CustomQuestionTypeContract.ts @@ -5,7 +5,7 @@ * Form authors create components that accept these props — no JSON Forms knowledge needed. * * Usage in JSON Schema: - * { "type": "string", "format": "rating-stars", "x-config": { "maxStars": 5 } } + * { "type": "string", "format": "rating-stars", "maxStars": 5 } * * Usage in custom_app: * custom_app/question_types/rating-stars/renderer.js @@ -21,8 +21,7 @@ export interface CustomQuestionTypeProps { /** * Configuration extracted from schema properties. - * Includes all properties alongside "format" (except reserved ones like type, title, etc.) - * and properties from "x-config" (x-config takes precedence). + * Includes all properties alongside "format" (except reserved ones like type, title, etc.). * For example, if schema has `"format": "rating", "maxStars": 5`, then `config.maxStars === 5`. */ config: Record; @@ -59,14 +58,15 @@ export interface CustomQuestionTypeProps { /** * Manifest passed from the native side describing available custom question types. - * Each entry maps a format string to the path of the module that renders it. + * Each entry maps a format string to the source code of the module that renders it. + * The RN side reads the JS file and passes the source string here for sandboxed evaluation. */ export interface CustomQuestionTypeManifest { custom_types: Record< string, { - /** Path to the JS module (e.g., "file:///path/to/question_types/rating-stars/index.js") */ - modulePath: string; + /** The JS source code of the module (read by RN via RNFS.readFile) */ + source: string; } >; } diff --git a/formulus/src/api/synkronus/index.ts b/formulus/src/api/synkronus/index.ts index 425311e0b..69d770803 100644 --- a/formulus/src/api/synkronus/index.ts +++ b/formulus/src/api/synkronus/index.ts @@ -154,45 +154,6 @@ class SynkronusApi { return response.data; } - /** - * Recursively removes a directory and all its contents. - * Handles nested directories and permission issues gracefully. - */ - private async removeDirectoryRecursive(path: string): Promise { - if (!(await RNFS.exists(path))) { - return; - } - - try { - const stat = await RNFS.stat(path); - if (stat.isDirectory()) { - try { - const items = await RNFS.readDir(path); - // Process items in reverse order to handle nested directories - for (let i = items.length - 1; i >= 0; i--) { - const item = items[i]; - const itemPath = `${path}/${item.name}`; - try { - if (item.isDirectory()) { - await this.removeDirectoryRecursive(itemPath); - } else { - await RNFS.unlink(itemPath); - } - } catch (_itemError) { - // Continue with other items even if one fails - } - } - } catch (_readDirError) { - // Continue to try removing the directory itself - } - } - await RNFS.unlink(path); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to clean directory ${path}: ${errorMsg}`); - } - } - /** * Downloads the app bundle as a single zip, extracts to a temp directory, * then atomically swaps into place so the old bundle stays intact until @@ -206,33 +167,14 @@ class SynkronusApi { this.fastGetToken_cachedToken ?? (await this.fastGetToken()); const zipUrl = `${config.basePath}/app-bundle/download-zip`; - // Use DocumentDirectoryPath for app data (more reliable than CachesDirectoryPath) - const tempZipPath = `${RNFS.DocumentDirectoryPath}/bundle_temp.zip`; - const tempExtractPath = `${RNFS.DocumentDirectoryPath}/bundle_staging`; + const tempZipPath = `${RNFS.CachesDirectoryPath}/bundle_temp.zip`; + const tempExtractPath = `${RNFS.CachesDirectoryPath}/bundle_staging`; const appDir = `${RNFS.DocumentDirectoryPath}/app`; const formsDir = `${RNFS.DocumentDirectoryPath}/forms`; // Clean up any leftover temp artifacts - if (await RNFS.exists(tempZipPath)) { - try { - await RNFS.unlink(tempZipPath); - } catch (_error) { - // Non-fatal, continue - } - } - - // Use a unique path for each extraction to avoid conflicts - const timestamp = Date.now(); - const actualExtractPath = `${RNFS.DocumentDirectoryPath}/bundle_staging_${timestamp}`; - - // Try to clean up old staging directories (non-fatal) - if (await RNFS.exists(tempExtractPath)) { - try { - await this.removeDirectoryRecursive(tempExtractPath); - } catch (_error) { - // Non-fatal - we're using a unique path anyway - } - } + if (await RNFS.exists(tempZipPath)) await RNFS.unlink(tempZipPath); + if (await RNFS.exists(tempExtractPath)) await RNFS.unlink(tempExtractPath); // Download the zip const downloadResult = await RNFS.downloadFile({ @@ -260,99 +202,28 @@ class SynkronusApi { progressCallback?.(50); - // Create fresh extract directory - try { - await RNFS.mkdir(actualExtractPath); - } catch (error) { - // Directory might already exist - if ( - !(error instanceof Error && error.message.includes('already exists')) - ) { - throw new Error( - `Failed to create extract directory: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - // Extract to staging directory - try { - await unzip(tempZipPath, actualExtractPath); - - // Verify extraction succeeded by checking if app directory exists - const stagingAppDir = `${actualExtractPath}/app`; - if (!(await RNFS.exists(stagingAppDir))) { - throw new Error( - 'Extraction completed but app directory not found in extracted files', - ); - } - - progressCallback?.(80); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - // Attempt to clean up the failed extraction directory - try { - await this.removeDirectoryRecursive(actualExtractPath); - } catch (_cleanupError) { - // Non-fatal cleanup error - } - throw new Error( - `Failed to extract bundle: ${errorMsg}. This may indicate a corrupted ZIP file or permission issue.`, - ); - } + await RNFS.mkdir(tempExtractPath); + await unzip(tempZipPath, tempExtractPath); + progressCallback?.(80); // Atomic swap: remove old dirs, move staging content into place - if (await RNFS.exists(appDir)) { - try { - await this.removeDirectoryRecursive(appDir); - } catch (_error) { - // Try direct unlink as fallback - try { - await RNFS.unlink(appDir); - } catch (_unlinkError) { - // Both methods failed, continue anyway - } - } - } - if (await RNFS.exists(formsDir)) { - try { - await this.removeDirectoryRecursive(formsDir); - } catch (_error) { - // Try direct unlink as fallback - try { - await RNFS.unlink(formsDir); - } catch (_unlinkError) { - // Both methods failed, continue anyway - } - } - } + if (await RNFS.exists(appDir)) await RNFS.unlink(appDir); + if (await RNFS.exists(formsDir)) await RNFS.unlink(formsDir); - const stagingAppDir = `${actualExtractPath}/app`; - const stagingFormsDir = `${actualExtractPath}/forms`; + const stagingAppDir = `${tempExtractPath}/app`; + const stagingFormsDir = `${tempExtractPath}/forms`; - if (await RNFS.exists(stagingAppDir)) { + if (await RNFS.exists(stagingAppDir)) await RNFS.moveFile(stagingAppDir, appDir); - } - if (await RNFS.exists(stagingFormsDir)) { + if (await RNFS.exists(stagingFormsDir)) await RNFS.moveFile(stagingFormsDir, formsDir); - } progressCallback?.(95); // Clean up temp files - if (await RNFS.exists(tempZipPath)) { - try { - await RNFS.unlink(tempZipPath); - } catch (_error) { - // Non-fatal cleanup error - } - } - if (await RNFS.exists(actualExtractPath)) { - try { - await this.removeDirectoryRecursive(actualExtractPath); - } catch (_error) { - // Non-fatal cleanup error - } - } + if (await RNFS.exists(tempZipPath)) await RNFS.unlink(tempZipPath); + if (await RNFS.exists(tempExtractPath)) await RNFS.unlink(tempExtractPath); progressCallback?.(100); } diff --git a/formulus/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx index cc09969eb..231c6ed24 100644 --- a/formulus/src/components/FormplayerModal.tsx +++ b/formulus/src/components/FormplayerModal.tsx @@ -35,7 +35,6 @@ import { databaseService } from '../database'; import { colors } from '../theme/colors'; import { FormSpec } from '../services'; // FormService will be imported directly import { ExtensionService } from '../services/ExtensionService'; -import { scanCustomQuestionTypes } from '../services/CustomQuestionTypeScanner'; import RNFS from 'react-native-fs'; import { useAppTheme } from '../contexts/AppThemeContext'; import { geolocationService } from '../services/GeolocationService'; @@ -314,19 +313,60 @@ const FormplayerModal = forwardRef( return; } - // Scan custom question types (reads JS files, screens against blocklist) + // Scan custom question types and read their source code + // Check app/question_types (bundle root) and app/forms/question_types (legacy) let customQuestionTypes = undefined; try { - const scanResult = await scanCustomQuestionTypes(customAppPath); - if (Object.keys(scanResult.custom_types).length > 0) { - customQuestionTypes = { - custom_types: scanResult.custom_types, - }; + const qtDirs = [ + `${customAppPath}/question_types`, + `${customAppPath}/forms/question_types`, + RNFS.DocumentDirectoryPath + '/forms/question_types', + ]; + + const custom_types: Record = {}; + + for (const qtDir of qtDirs) { + const qtDirExists = await RNFS.exists(qtDir); + if (!qtDirExists) { + continue; + } + + const folders = await RNFS.readDir(qtDir); + + for (const folder of folders) { + if (folder.isDirectory() && !custom_types[folder.name]) { + // Try renderer.js first, then index.js as fallback + const rendererPath = `${folder.path}/renderer.js`; + const indexPath = `${folder.path}/index.js`; + const hasRenderer = await RNFS.exists(rendererPath); + const hasIndex = !hasRenderer && (await RNFS.exists(indexPath)); + const jsPath = hasRenderer + ? rendererPath + : hasIndex + ? indexPath + : null; + + if (jsPath) { + // Read the source code so the WebView can evaluate it directly + const source = await RNFS.readFile(jsPath, 'utf8'); + custom_types[folder.name] = { source }; + console.log( + `[FormplayerModal] Custom question type: "${folder.name}" (${source.length} bytes from ${jsPath})`, + ); + } else { + console.warn( + `[FormplayerModal] Skipping "${folder.name}": no renderer.js or index.js found`, + ); + } + } + } } - if (scanResult.errors.length > 0) { + + if (Object.keys(custom_types).length > 0) { + customQuestionTypes = { custom_types }; + } else { console.warn( - 'Some custom question types failed screening:', - scanResult.errors, + '[FormplayerModal] No custom question types found in any path', ); } } catch (error) { diff --git a/formulus/src/services/CustomQuestionTypeScanner.ts b/formulus/src/services/CustomQuestionTypeScanner.ts deleted file mode 100644 index e1bc522d8..000000000 --- a/formulus/src/services/CustomQuestionTypeScanner.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * CustomQuestionTypeScanner.ts - * - * Scans the custom_app's `question_types/` directory on the device filesystem, - * reads each module's source code (from renderer.js files), and screens it - * against a blocklist of dangerous patterns before passing it to FormPlayer. - * - * This runs on the Formulus RN side (not in the WebView). - * - * Security: This is the first line of defense. Source code that contains - * dangerous API calls is rejected before it ever reaches the WebView. - * - * File structure: - * question_types/{formatName}/renderer.js - * - * Schema usage: - * { "type": "string", "format": "{formatName}", ... } - * The format name must match the directory name. - */ - -import RNFS from 'react-native-fs'; - -export interface ScannedQuestionType { - /** The raw JS source code of the module */ - source: string; -} - -export interface ScanResult { - /** Successfully scanned custom question types, keyed by format name (folder name) */ - custom_types: Record; - /** Errors encountered during scanning (types that were rejected or couldn't be read) */ - errors: Array<{ name: string; error: string }>; -} - -/** - * Patterns that indicate potentially dangerous code. - * If any of these are found in the source, the module is rejected. - */ -const BLOCKED_PATTERNS: Array<{ pattern: RegExp; description: string }> = [ - { pattern: /\bfetch\s*\(/, description: 'Network request via fetch()' }, - { - pattern: /\bXMLHttpRequest\b/, - description: 'Network request via XMLHttpRequest', - }, - { pattern: /\bWebSocket\b/, description: 'WebSocket connection' }, - { pattern: /\beval\s*\(/, description: 'Dynamic code evaluation via eval()' }, - { - pattern: /\bnew\s+Function\s*\(/, - description: 'Dynamic code evaluation via new Function()', - }, - { pattern: /\bdocument\.cookie\b/, description: 'Cookie access' }, - { pattern: /\blocalStorage\b/, description: 'localStorage access' }, - { pattern: /\bsessionStorage\b/, description: 'sessionStorage access' }, - { pattern: /\bindexedDB\b/, description: 'IndexedDB access' }, - { - pattern: /\bnavigator\.sendBeacon\b/, - description: 'Data exfiltration via sendBeacon', - }, - { - pattern: /\bimportScripts\s*\(/, - description: 'Script import via importScripts()', - }, -]; - -/** - * Screen source code against the blocklist. - * Returns null if the source is clean, or a description of the violation. - */ -function screenSource(source: string): string | null { - for (const { pattern, description } of BLOCKED_PATTERNS) { - if (pattern.test(source)) { - return description; - } - } - return null; -} - -/** - * Scan the `question_types/` directory inside the custom app path. - * - * For each subdirectory found: - * 1. Check for a `renderer.js` file - * 2. Read the file contents as a string - * 3. Screen the source against the blocklist - * 4. If clean, include in the result - * - * The directory name becomes the format name used in schemas. - * Example: "ranking/" directory → use "format": "ranking" in schema - * - * @param customAppPath - The root path of the custom app (e.g., RNFS.DocumentDirectoryPath + '/app') - * @returns Scanned question types and any errors - */ -export async function scanCustomQuestionTypes( - customAppPath: string, -): Promise { - const result: ScanResult = { - custom_types: {}, - errors: [], - }; - - const questionTypesDir = `${customAppPath}/question_types`; - - const dirExists = await RNFS.exists(questionTypesDir); - - // Check if the question_types directory exists - if (!dirExists) { - console.log( - '[CustomQuestionTypeScanner] No question_types/ directory found at:', - questionTypesDir, - ); - return result; - } - - // Read all items in the question_types directory - let folders: RNFS.ReadDirItem[]; - try { - folders = await RNFS.readDir(questionTypesDir); - } catch (err) { - console.error( - '[CustomQuestionTypeScanner] Failed to read question_types directory:', - err, - ); - return result; - } - - // Process each subdirectory - for (const folder of folders) { - if (!folder.isDirectory()) { - continue; - } - - const formatName = folder.name; // e.g., "ranking" - const rendererPath = `${folder.path}/renderer.js`; - - try { - // Check if renderer.js exists - const fileExists = await RNFS.exists(rendererPath); - if (!fileExists) { - result.errors.push({ - name: formatName, - error: `No renderer.js found in question_types/${formatName}/`, - }); - continue; - } - - // Read the source code - const source = await RNFS.readFile(rendererPath, 'utf8'); - - if (!source || source.trim().length === 0) { - result.errors.push({ - name: formatName, - error: 'renderer.js is empty', - }); - continue; - } - - // Screen against the blocklist - const violation = screenSource(source); - if (violation) { - result.errors.push({ - name: formatName, - error: `Blocked: ${violation}`, - }); - console.warn( - `[CustomQuestionTypeScanner] Rejected "${formatName}": ${violation}`, - ); - continue; - } - - // Source is clean — include it - result.custom_types[formatName] = { source }; - console.log( - `[CustomQuestionTypeScanner] Accepted "${formatName}" (${source.length} bytes)`, - ); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : String(err); - result.errors.push({ - name: formatName, - error: `Failed to read: ${errorMessage}`, - }); - console.error( - `[CustomQuestionTypeScanner] Error processing "${formatName}":`, - errorMessage, - ); - } - } - - console.log( - `[CustomQuestionTypeScanner] Scan complete: ${Object.keys(result.custom_types).length} accepted, ${result.errors.length} errors`, - ); - - return result; -} From 54215e053cae6368f7db2b59771fca0b97aef81e Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Mon, 2 Mar 2026 10:06:11 +0300 Subject: [PATCH 23/25] chore: remove build artifact from tracking - Remove formulus/android/app/src/main/assets/formplayer_dist/index.html from git tracking - This file is auto-generated and should not be committed - Already in .gitignore, now properly untracked --- .github/ISSUE_TEMPLATE/testcase_cli.md | 20 + .github/ISSUE_TEMPLATE/testcase_e2e.md | 20 + .github/ISSUE_TEMPLATE/testcase_formulus.md | 20 + .github/ISSUE_TEMPLATE/testcase_synkronus.md | 20 + .github/workflows/synkronus-docker.yml | 28 +- Dockerfile | 8 +- formulus-formplayer/package-lock.json | 150 +-- formulus-formplayer/src/App.tsx | 26 +- formulus/App.tsx | 95 +- formulus/android/app/build.gradle | 17 +- .../main/assets/formplayer_dist/index.html | 24 - .../main/assets/images/welcome-bg-dark.png | Bin 0 -> 31452 bytes .../main/assets/images/welcome-bg-light.png | Bin 0 -> 166942 bytes .../main/assets/webview/placeholder_app.html | 232 ++++- formulus/assets/images/welcome-bg-dark.png | Bin 0 -> 31452 bytes formulus/assets/images/welcome-bg-light.png | Bin 0 -> 166942 bytes formulus/assets/webview/placeholder_app.html | 232 ++++- formulus/eslint.config.js | 8 + formulus/index.js | 6 + formulus/package-lock.json | 200 +++- formulus/package.json | 13 +- formulus/scripts/generatePlaceholderTokens.js | 139 +++ formulus/scripts/syncNativeVersion.js | 107 +++ formulus/src/api/synkronus/Auth.ts | 24 +- .../src/api/synkronus/__tests__/Auth.test.ts | 40 +- formulus/src/api/synkronus/client.ts | 30 + formulus/src/api/synkronus/download.ts | 41 + formulus/src/api/synkronus/index.ts | 16 +- .../components/BlurredScreenBackground.tsx | 122 +++ formulus/src/components/CustomAppWebView.tsx | 514 ++++++----- formulus/src/components/FormplayerModal.tsx | 116 ++- formulus/src/components/MenuDrawer.tsx | 520 ++++++++--- .../src/components/QRScannerModalImpl.tsx | 76 +- formulus/src/components/common/Button.tsx | 23 +- .../src/components/common/ConfirmModal.tsx | 156 ++++ formulus/src/components/common/EmptyState.tsx | 16 +- formulus/src/components/common/FormCard.tsx | 104 ++- .../components/common/FormTypeSelector.tsx | 163 ++-- formulus/src/components/common/Input.tsx | 70 +- .../src/components/common/ObservationCard.tsx | 158 ++-- formulus/src/components/common/index.ts | 2 + formulus/src/contexts/AppThemeContext.tsx | 53 +- formulus/src/contexts/ConfirmModalContext.tsx | 81 ++ formulus/src/errors/VersionMismatchError.ts | 22 + formulus/src/navigation/MainTabNavigator.tsx | 320 ++++++- formulus/src/screens/AboutScreen.tsx | 261 ++++-- formulus/src/screens/FormManagementScreen.tsx | 31 +- formulus/src/screens/FormsScreen.tsx | 254 ++++-- formulus/src/screens/HelpScreen.tsx | 242 +++-- formulus/src/screens/HomeScreen.tsx | 70 +- formulus/src/screens/MoreScreen.tsx | 77 +- .../src/screens/ObservationDetailScreen.tsx | 17 +- formulus/src/screens/ObservationsScreen.tsx | 341 ++++--- formulus/src/screens/SettingsScreen.tsx | 229 +++-- formulus/src/screens/SyncScreen.tsx | 860 +++++++++++------- formulus/src/screens/WelcomeScreen.tsx | 160 +++- formulus/src/services/AppConfigService.ts | 2 +- formulus/src/services/SyncService.ts | 15 +- formulus/src/theme/colors.ts | 54 ++ formulus/src/theme/odeDesign.ts | 56 ++ formulus/src/types/NavigationTypes.ts | 8 +- formulus/src/version.ts | 10 + packages/components/src/react-web/Button.tsx | 7 +- packages/tokens/README.md | 12 + .../tokens/accessibility/touch-targets.json | 1 + .../tokens/src/tokens/components/button.json | 18 +- scripts/build-synkronus.sh | 47 + synkronus-portal/package-lock.json | 14 +- synkronus/Dockerfile | 9 +- synkronus/cmd/synkronus/main.go | 2 +- synkronus/internal/api/api.go | 5 +- .../formulusversion/formulusversion.go | 91 ++ .../formulusversion/formulusversion_test.go | 89 ++ synkronus/pkg/version/service.go | 11 +- synkronus/pkg/version/service_test.go | 41 + synkronus/portal/handler_test.go | 41 + 76 files changed, 5261 insertions(+), 1846 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/testcase_cli.md create mode 100644 .github/ISSUE_TEMPLATE/testcase_e2e.md create mode 100644 .github/ISSUE_TEMPLATE/testcase_formulus.md create mode 100644 .github/ISSUE_TEMPLATE/testcase_synkronus.md delete mode 100644 formulus/android/app/src/main/assets/formplayer_dist/index.html create mode 100644 formulus/android/app/src/main/assets/images/welcome-bg-dark.png create mode 100644 formulus/android/app/src/main/assets/images/welcome-bg-light.png create mode 100644 formulus/assets/images/welcome-bg-dark.png create mode 100644 formulus/assets/images/welcome-bg-light.png create mode 100644 formulus/scripts/generatePlaceholderTokens.js create mode 100644 formulus/scripts/syncNativeVersion.js create mode 100644 formulus/src/api/synkronus/client.ts create mode 100644 formulus/src/api/synkronus/download.ts create mode 100644 formulus/src/components/BlurredScreenBackground.tsx create mode 100644 formulus/src/components/common/ConfirmModal.tsx create mode 100644 formulus/src/contexts/ConfirmModalContext.tsx create mode 100644 formulus/src/errors/VersionMismatchError.ts create mode 100644 formulus/src/theme/odeDesign.ts create mode 100644 formulus/src/version.ts create mode 100755 scripts/build-synkronus.sh create mode 100644 synkronus/pkg/middleware/formulusversion/formulusversion.go create mode 100644 synkronus/pkg/middleware/formulusversion/formulusversion_test.go create mode 100644 synkronus/pkg/version/service_test.go create mode 100644 synkronus/portal/handler_test.go diff --git a/.github/ISSUE_TEMPLATE/testcase_cli.md b/.github/ISSUE_TEMPLATE/testcase_cli.md new file mode 100644 index 000000000..dc11817e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/testcase_cli.md @@ -0,0 +1,20 @@ +--- +name: "Testcase [CLI]" +about: "Test case for CLI" +labels: ["test", "test:CLI"] +--- + +## Description +Describe what this test case covers. + +## Steps +1. Step-by-step instructions. + +## Expected Result +What should happen. + +## Actual Result +(To be filled during testing) + +## Priority +High / Medium / Low diff --git a/.github/ISSUE_TEMPLATE/testcase_e2e.md b/.github/ISSUE_TEMPLATE/testcase_e2e.md new file mode 100644 index 000000000..7daeb8a52 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/testcase_e2e.md @@ -0,0 +1,20 @@ +--- +name: "Testcase [E2E]" +about: "End-to-end test case across multiple components" +labels: ["test", "test:E2E"] +--- + +## Description +Describe what this end-to-end test covers. + +## Steps +1. Step-by-step instructions. + +## Expected Result +What should happen. + +## Actual Result +(To be filled during testing) + +## Priority +High / Medium / Low diff --git a/.github/ISSUE_TEMPLATE/testcase_formulus.md b/.github/ISSUE_TEMPLATE/testcase_formulus.md new file mode 100644 index 000000000..1f513652c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/testcase_formulus.md @@ -0,0 +1,20 @@ +--- +name: "Testcase [Formulus]" +about: "Test case for Formulus / FormPlayer" +labels: ["test", "test:formulus"] +--- + +## Description +Describe what this test case covers in Formulus / FormPlayer. + +## Steps +1. Step-by-step instructions. + +## Expected Result +What should happen. + +## Actual Result +(To be filled during testing) + +## Priority +High / Medium / Low diff --git a/.github/ISSUE_TEMPLATE/testcase_synkronus.md b/.github/ISSUE_TEMPLATE/testcase_synkronus.md new file mode 100644 index 000000000..5e794db42 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/testcase_synkronus.md @@ -0,0 +1,20 @@ +--- +name: "Testcase [Synkronus]" +about: "Test case for Synkronus / Portal" +labels: ["test", "test:synkronus"] +--- + +## Description +Describe what this test case covers in Synkronus/Portal. + +## Steps +1. Step-by-step instructions. + +## Expected Result +What should happen. + +## Actual Result +(To be filled during testing) + +## Priority +High / Medium / Low diff --git a/.github/workflows/synkronus-docker.yml b/.github/workflows/synkronus-docker.yml index 274442800..81e0415df 100644 --- a/.github/workflows/synkronus-docker.yml +++ b/.github/workflows/synkronus-docker.yml @@ -19,11 +19,6 @@ on: - 'Dockerfile' - '.github/workflows/synkronus-docker.yml' workflow_dispatch: - inputs: - version: - description: 'Version tag (e.g., v1.0.0). If empty, tags will be derived from the current ref.' - required: false - type: string release: types: [published] @@ -59,6 +54,24 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Fetch git tags for version detection + run: | + # Fetch all tags to ensure git describe works correctly + git fetch --tags --quiet || true + + - name: Determine Synkronus version from git + id: version + run: | + # Version is automatically derived from git tags (standard Go practice) + # For releases, use the release tag; otherwise use git describe + if [ "${{ github.event_name }}" == "release" ]; then + VERSION="${{ github.event.release.tag_name }}" + else + VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev") + fi + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "Building Synkronus with version: ${VERSION}" + - name: Extract metadata (tags, labels) id: meta uses: docker/metadata-action@v5 @@ -70,9 +83,6 @@ jobs: # For dev branch: tag as both dev and latest (for demo server auto-updates) type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }} type=raw,value=latest,enable=${{ github.ref == 'refs/heads/dev' }} - # When triggered via manual dispatch with a version input - type=semver,pattern=v{{version}},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }},value=${{ github.event.inputs.version }} - type=semver,pattern=v{{major}}.{{minor}},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }},value=${{ github.event.inputs.version }} # When triggered from a GitHub Release event, use the release tag name as the semver source type=semver,pattern=v{{version}},enable=${{ github.event_name == 'release' }},value=${{ github.event.release.tag_name }} type=semver,pattern=v{{major}}.{{minor}},enable=${{ github.event_name == 'release' }},value=${{ github.event.release.tag_name }} @@ -97,6 +107,8 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + SYNKRONUS_VERSION=${{ steps.version.outputs.version }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index a7f0ae039..e0c979427 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,9 +62,13 @@ COPY synkronus/ ./ # Embed the portal build into the binary (must exist at go build time) COPY --from=portal-builder /app/synkronus-portal/dist ./portal/dist -# Build the application +# Build the application with version from build arg +# Version is automatically derived from git tags in CI and passed as build arg +# Defaults to "dev" for local builds without tags +ARG SYNKRONUS_VERSION=dev ENV CGO_ENABLED=0 GOOS=linux -RUN go build -a -ldflags='-w -s' -o synkronus ./cmd/synkronus +RUN echo "Building Synkronus with version: ${SYNKRONUS_VERSION}" && \ + go build -a -ldflags="-w -s -X github.com/opendataensemble/synkronus/pkg/version.version=${SYNKRONUS_VERSION}" -o synkronus ./cmd/synkronus # Stage 3: Minimal runtime image — single Go server (API + portal) FROM alpine:3.19 diff --git a/formulus-formplayer/package-lock.json b/formulus-formplayer/package-lock.json index 6afff12ce..a1b57f010 100644 --- a/formulus-formplayer/package-lock.json +++ b/formulus-formplayer/package-lock.json @@ -1167,9 +1167,9 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "license": "MIT", "engines": { @@ -3860,9 +3860,9 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "dependencies": { @@ -3872,7 +3872,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -4091,9 +4091,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -4120,23 +4120,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4144,54 +4127,6 @@ "dev": true, "license": "MIT" }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -4343,6 +4278,23 @@ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", "license": "MIT" }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -5287,6 +5239,22 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -5558,6 +5526,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/formulus-formplayer/src/App.tsx b/formulus-formplayer/src/App.tsx index fa28cdbf3..8ab18b5c0 100644 --- a/formulus-formplayer/src/App.tsx +++ b/formulus-formplayer/src/App.tsx @@ -900,24 +900,26 @@ function App() { ); }, [darkMode, customThemeColors]); - // Set CSS custom properties from tokens for use in CSS files - // Must be called before any early returns to follow React Hooks rules + // Set CSS custom properties for use in CSS files and by ODE Button. + // When a custom app provides themeColors, use those so buttons and other + // token-based UI match the app branding; otherwise use default tokens. useEffect(() => { const root = document.documentElement; + const primary = + customThemeColors?.primary ?? tokens.color.brand.primary[500]; + const onPrimary = + customThemeColors?.onPrimary ?? tokens.color.neutral.white; + root.style.setProperty('--ode-color-brand-primary-500', primary); + root.style.setProperty('--ode-color-neutral-white', onPrimary); root.style.setProperty( - '--ode-color-brand-primary-500', - tokens.color.brand.primary[500], - ); - root.style.setProperty( - '--ode-color-neutral-white', - tokens.color.neutral.white, + '--ode-color-neutral-200', + customThemeColors?.onSurface ?? tokens.color.neutral[200], ); root.style.setProperty( - '--ode-color-neutral-200', - tokens.color.neutral[200], + '--ode-color-neutral-50', + customThemeColors?.surface ?? tokens.color.neutral[50], ); - root.style.setProperty('--ode-color-neutral-50', tokens.color.neutral[50]); - }, []); + }, [customThemeColors]); // Show draft selector if we have pending form init and available drafts if (showDraftSelector && pendingFormInit) { diff --git a/formulus/App.tsx b/formulus/App.tsx index d5ebabe87..259c3acee 100644 --- a/formulus/App.tsx +++ b/formulus/App.tsx @@ -4,12 +4,13 @@ import { DefaultTheme, DarkTheme, } from '@react-navigation/native'; -import { StatusBar, useColorScheme } from 'react-native'; +import { StatusBar, Alert } from 'react-native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import 'react-native-url-polyfill/auto'; import { FormService } from './src/services/FormService'; import { SyncProvider } from './src/contexts/SyncContext'; import { AppThemeProvider, useAppTheme } from './src/contexts/AppThemeContext'; +import { ConfirmModalProvider } from './src/contexts/ConfirmModalContext'; import { appEvents, Listener } from './src/webview/FormulusMessageHandlers.ts'; import FormplayerModal, { FormplayerModalHandle, @@ -24,15 +25,15 @@ import { FormInitData } from './src/webview/FormulusInterfaceDefinition.ts'; * React Navigation theme matching the custom app's branding. */ function AppInner(): React.JSX.Element { - const colorScheme = useColorScheme(); - const { themeColors } = useAppTheme(); + const { themeColors, resolvedMode } = useAppTheme(); + const isDark = resolvedMode === 'dark'; // Build the React Navigation theme dynamically from the custom app's colors. const navigationTheme = useMemo(() => { - const base = colorScheme === 'dark' ? DarkTheme : DefaultTheme; + const base = isDark ? DarkTheme : DefaultTheme; return { ...base, - dark: colorScheme === 'dark', + dark: isDark, colors: { ...base.colors, primary: themeColors.primary, @@ -43,7 +44,7 @@ function AppInner(): React.JSX.Element { notification: themeColors.error, }, }; - }, [colorScheme, themeColors]); + }, [isDark, themeColors]); const [qrScannerVisible, setQrScannerVisible] = useState(false); const [qrScannerData, setQrScannerData] = useState<{ @@ -91,34 +92,68 @@ function AppInner(): React.JSX.Element { ); const handleOpenFormplayer = async (config: FormInitData) => { + // If formplayer is already visible, close it first to allow opening a new form if (formplayerVisibleRef.current) { - return; + console.log( + '[App] Formplayer already visible, closing first before opening new form', + ); + formplayerVisibleRef.current = false; + setFormplayerVisible(false); + // Wait for modal to close before proceeding + await new Promise(resolve => setTimeout(() => resolve(), 300)); } const { formType, observationId, params, savedData, operationId } = config; - formplayerVisibleRef.current = true; - setFormplayerVisible(true); - const formService = await FormService.getInstance(); - const forms = formService.getFormSpecs(); - - if (forms.length === 0) { - return; - } - - const formSpec = forms.find(form => form.id === formType); - if (!formSpec) { - return; + try { + const formService = await FormService.getInstance(); + const forms = formService.getFormSpecs(); + + if (forms.length === 0) { + Alert.alert( + 'No Forms Available', + 'No forms are available. Please sync forms first.', + ); + return; + } + + const formSpec = forms.find(form => form.id === formType); + if (!formSpec) { + Alert.alert( + 'Form Not Found', + `Form "${formType}" not found. Please sync forms first.`, + ); + return; + } + + // Set visible state first to mount the modal + formplayerVisibleRef.current = true; + setFormplayerVisible(true); + + // Wait for modal to mount and WebView to start loading before initializing form + // This ensures the WebView ref is available and the modal is visible + setTimeout(() => { + formplayerModalRef.current?.initializeForm( + formSpec, + params || null, + observationId || null, + savedData || null, + operationId || null, + ); + }, 200); + } catch (error) { + console.error('[App] Error opening formplayer:', error); + Alert.alert( + 'Error', + `Failed to open form: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + ); + // Reset state on error + formplayerVisibleRef.current = false; + setFormplayerVisible(false); } - - formplayerModalRef.current?.initializeForm( - formSpec, - params || null, - observationId || null, - savedData || null, - operationId || null, - ); }; const handleCloseFormplayer = () => { @@ -152,7 +187,7 @@ function AppInner(): React.JSX.Element { return ( <> @@ -201,7 +236,9 @@ function App(): React.JSX.Element { - + + + diff --git a/formulus/android/app/build.gradle b/formulus/android/app/build.gradle index 8dc6cc358..7aeb37f8b 100644 --- a/formulus/android/app/build.gradle +++ b/formulus/android/app/build.gradle @@ -93,7 +93,7 @@ android { minSdk = rootProject.ext.minSdkVersion targetSdk = rootProject.ext.targetSdkVersion versionCode = 1 - versionName = "1.0" + versionName = "1.0.0" // Enable VisionCamera code scanner buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", (findProperty("newArchEnabled") ?: "false").toString() @@ -182,6 +182,11 @@ dependencies { } +// Generate placeholder :root from @ode/tokens so token changes propagate +task generatePlaceholderTokens(type: Exec) { + workingDir "${rootDir}/.." + commandLine 'node', 'scripts/generatePlaceholderTokens.js' +} task copyWebviewAssets(type: Copy) { from "${rootDir}/../assets/webview" into "${projectDir}/src/main/assets/webview" @@ -189,4 +194,12 @@ task copyWebviewAssets(type: Copy) { include '**/*.jsx' include '**/*.html' } -preBuild.dependsOn(copyWebviewAssets) \ No newline at end of file +task copyPlaceholderImages(type: Copy) { + from "${rootDir}/../assets/images" + into "${projectDir}/src/main/assets/images" + include 'welcome-bg-light.png' + include 'welcome-bg-dark.png' +} +copyWebviewAssets.dependsOn(generatePlaceholderTokens) +preBuild.dependsOn(copyWebviewAssets) +preBuild.dependsOn(copyPlaceholderImages) \ No newline at end of file diff --git a/formulus/android/app/src/main/assets/formplayer_dist/index.html b/formulus/android/app/src/main/assets/formplayer_dist/index.html deleted file mode 100644 index f9b4e24f6..000000000 --- a/formulus/android/app/src/main/assets/formplayer_dist/index.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - Formulus Form Player - - - - - - - - -
- - - - diff --git a/formulus/android/app/src/main/assets/images/welcome-bg-dark.png b/formulus/android/app/src/main/assets/images/welcome-bg-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..aabdd57bb286c14ddb6040df2f99b395b51af9d5 GIT binary patch literal 31452 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 literal 0 HcmV?d00001 diff --git a/formulus/android/app/src/main/assets/webview/placeholder_app.html b/formulus/android/app/src/main/assets/webview/placeholder_app.html index ee390c8d7..c8738a5b2 100644 --- a/formulus/android/app/src/main/assets/webview/placeholder_app.html +++ b/formulus/android/app/src/main/assets/webview/placeholder_app.html @@ -1,56 +1,226 @@ - + Custom App Placeholder -

Your Custom App

-

This is a placeholder. Your app will appear here after sync.

+
+

Your Custom
App

+

Login and Sync to load your forms!

+ +

Secure • Fast • Offline

+

v0.1.0-native

+
diff --git a/formulus/assets/images/welcome-bg-dark.png b/formulus/assets/images/welcome-bg-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..aabdd57bb286c14ddb6040df2f99b395b51af9d5 GIT binary patch literal 31452 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
po#A$a< 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 literal 0 HcmV?d00001 diff --git a/formulus/assets/webview/placeholder_app.html b/formulus/assets/webview/placeholder_app.html index ee390c8d7..c8738a5b2 100644 --- a/formulus/assets/webview/placeholder_app.html +++ b/formulus/assets/webview/placeholder_app.html @@ -1,56 +1,226 @@ - + Custom App Placeholder -

Your Custom App

-

This is a placeholder. Your app will appear here after sync.

+
+

Your Custom
App

+

Login and Sync to load your forms!

+ +

Secure • Fast • Offline

+

v0.1.0-native

+
diff --git a/formulus/eslint.config.js b/formulus/eslint.config.js index 645721d3e..e67cab9c0 100644 --- a/formulus/eslint.config.js +++ b/formulus/eslint.config.js @@ -57,4 +57,12 @@ export default tseslint.config( }, }, prettierConfig, + { + files: ['scripts/**/*.js'], + languageOptions: { + globals: { + ...globals.node, + }, + }, + }, ); diff --git a/formulus/index.js b/formulus/index.js index f60254560..d276d516d 100644 --- a/formulus/index.js +++ b/formulus/index.js @@ -4,9 +4,15 @@ import { AppRegistry, Platform } from 'react-native'; import notifee from '@notifee/react-native'; +// Initialize axios interceptors BEFORE any other imports that might make API calls +// This ensures version mismatch errors are handled from the very first request +import { setupSynkronusClientInterceptors } from './src/api/synkronus/client'; import App from './App'; import { name as appName } from './app.json'; +// Set up interceptors immediately - before any React components or contexts load +setupSynkronusClientInterceptors(); + if (Platform.OS === 'android') { notifee.registerForegroundService(() => { return new Promise(() => {}); diff --git a/formulus/package-lock.json b/formulus/package-lock.json index e71be7226..d835f4b14 100644 --- a/formulus/package-lock.json +++ b/formulus/package-lock.json @@ -13,7 +13,9 @@ "@ode/components": "file:../packages/components", "@ode/tokens": "file:../packages/tokens", "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-community/blur": "^4.4.1", "@react-native-documents/picker": "^12.0.0", + "@react-native-vector-icons/fontawesome6": "^12.3.1", "@react-native-vector-icons/material-design-icons": "^12.4.0", "@react-native-vector-icons/material-icons": "^12.4.0", "@react-navigation/bottom-tabs": "^7.9.0", @@ -52,7 +54,7 @@ "@react-native-community/cli": "20.0.2", "@react-native-community/cli-platform-android": "20.0.2", "@react-native-community/cli-platform-ios": "20.0.2", - "@react-native/babel-preset": "0.83.1", + "@react-native/babel-preset": "0.84.0", "@react-native/eslint-config": "0.83.1", "@react-native/metro-config": "0.83.1", "@react-native/typescript-config": "0.83.1", @@ -2301,20 +2303,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" }, "engines": { @@ -2338,9 +2340,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "license": "MIT", "engines": { @@ -3620,6 +3622,16 @@ "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, + "node_modules/@react-native-community/blur": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@react-native-community/blur/-/blur-4.4.1.tgz", + "integrity": "sha512-XBSsRiYxE/MOEln2ayunShfJtWztHwUxLFcSL20o+HNNRnuUDv+GXkF6FmM2zE8ZUfrnhQ/zeTqvnuDPGw6O8A==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native-community/cli": { "version": "20.0.2", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.0.2.tgz", @@ -4024,6 +4036,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@react-native-vector-icons/fontawesome6": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@react-native-vector-icons/fontawesome6/-/fontawesome6-12.3.1.tgz", + "integrity": "sha512-osce83O1X1fbGD09qHi/N7XI76n4+JDjLtbT7tFDq1+2reY9p1lNwGWdQN1s+X/7eNaq0P599Uu7tMn7juQvOA==", + "license": "MIT", + "dependencies": { + "@react-native-vector-icons/common": "^12.4.0" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native-vector-icons/material-design-icons": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/@react-native-vector-icons/material-design-icons/-/material-design-icons-12.4.0.tgz", @@ -4041,9 +4069,9 @@ } }, "node_modules/@react-native-vector-icons/material-icons": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/@react-native-vector-icons/material-icons/-/material-icons-12.4.0.tgz", - "integrity": "sha512-R0C2BBoZZ5sjZJYLbFx1vfyYwfbOcBHRYzRHDKUlsCdplc34HLa0JyHYGMGx3q5xHAJB6Dl4N2cmZUVALuJ60w==", + "version": "12.4.1", + "resolved": "https://registry.npmjs.org/@react-native-vector-icons/material-icons/-/material-icons-12.4.1.tgz", + "integrity": "sha512-xUgYPEptDJ955AJcTvIog68jZTav88wX+/zfyuTHbgZFj3xu907DnKU3L/asGqbZztwM6tiAOpNKeySM5PEoqw==", "license": "MIT", "dependencies": { "@react-native-vector-icons/common": "^12.4.0" @@ -4066,24 +4094,46 @@ } }, "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.83.1", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.83.1.tgz", - "integrity": "sha512-VPj8O3pG1ESjZho9WVKxqiuryrotAECPHGF5mx46zLUYNTWR5u9OMUXYk7LeLy+JLWdGEZ2Gn3KoXeFZbuqE+g==", - "devOptional": true, + "version": "0.84.0", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.84.0.tgz", + "integrity": "sha512-8GGVqcfZQnpmaud1GBww/Z8tF5qaWvork5E+TTTQQm7l0p2WnYkzCDJdZOdISHwSO6ikAjh998c3CVPubij3rQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.3", - "@react-native/codegen": "0.83.1" + "@react-native/codegen": "0.84.0" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/babel-plugin-codegen/node_modules/@react-native/codegen": { + "version": "0.84.0", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.84.0.tgz", + "integrity": "sha512-TcTAO58JigCw9onYTrbE2yK2js5YNgqbmnpYyq9oXz2mofbX7JcK53kIi7fhqyJhie8RkY+X85zSOTWNs6S3CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/parser": "^7.25.3", + "hermes-parser": "0.32.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "tinyglobby": "^0.2.15", + "yargs": "^17.6.2" }, "engines": { "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" } }, "node_modules/@react-native/babel-preset": { - "version": "0.83.1", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.83.1.tgz", - "integrity": "sha512-xI+tbsD4fXcI6PVU4sauRCh0a5fuLQC849SINmU2J5wP8kzKu4Ye0YkGjUW3mfGrjaZcjkWmF6s33jpyd3gdTw==", - "devOptional": true, + "version": "0.84.0", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.84.0.tgz", + "integrity": "sha512-X7QfJCRyvawFUzAwidKynOh9Wc36r/OK+lEweNGyRCmciqVxs/8J/HAnANBks/kM/z7XlepG0hU1D/VjHKA/6g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -4092,27 +4142,19 @@ "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", - "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-function-name": "^7.25.1", - "@babel/plugin-transform-literals": "^7.25.2", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-numeric-separator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", @@ -4121,13 +4163,9 @@ "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/plugin-transform-shorthand-properties": "^7.24.7", - "@babel/plugin-transform-spread": "^7.24.7", - "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/template": "^7.25.0", - "@react-native/babel-plugin-codegen": "0.83.1", + "@react-native/babel-plugin-codegen": "0.84.0", "babel-plugin-syntax-hermes-parser": "0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" @@ -4436,6 +4474,80 @@ "@babel/core": "*" } }, + "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-plugin-codegen": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.83.1.tgz", + "integrity": "sha512-VPj8O3pG1ESjZho9WVKxqiuryrotAECPHGF5mx46zLUYNTWR5u9OMUXYk7LeLy+JLWdGEZ2Gn3KoXeFZbuqE+g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.83.1" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-preset": { + "version": "0.83.1", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.83.1.tgz", + "integrity": "sha512-xI+tbsD4fXcI6PVU4sauRCh0a5fuLQC849SINmU2J5wP8kzKu4Ye0YkGjUW3mfGrjaZcjkWmF6s33jpyd3gdTw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.83.1", + "babel-plugin-syntax-hermes-parser": "0.32.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, "node_modules/@react-native/metro-config": { "version": "0.83.1", "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.83.1.tgz", @@ -5615,9 +5727,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -7705,9 +7817,9 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "dependencies": { @@ -7717,7 +7829,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -12154,9 +12266,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" diff --git a/formulus/package.json b/formulus/package.json index 4769f80f9..53d9c8853 100644 --- a/formulus/package.json +++ b/formulus/package.json @@ -1,6 +1,6 @@ { "name": "formulus-app", - "version": "0.0.1", + "version": "1.0.0", "type": "module", "private": true, "scripts": { @@ -15,15 +15,18 @@ "generate": "ts-node --project scripts/tsconfig.json scripts/generateInjectionScript.ts", "generate:api": "openapi-generator-cli generate -i ../synkronus/openapi/synkronus.yaml -g typescript-axios -o src/api/synkronus/generated --additional-properties=supportsES6=true,useSingleRequestParameter=true,modelPropertyNaming=original", "generate_qr": "ts-node --project scripts/tsconfig.json scripts/generateQR.ts", - "prebuild": "npm run generate" + "sync:version": "node scripts/syncNativeVersion.js", + "prebuild": "npm run sync:version && npm run generate" }, "dependencies": { - "@ode/components": "file:../packages/components", - "@ode/tokens": "file:../packages/tokens", "@notifee/react-native": "^9.1.8", "@nozbe/watermelondb": "^0.28.0", + "@ode/components": "file:../packages/components", + "@ode/tokens": "file:../packages/tokens", "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-community/blur": "^4.4.1", "@react-native-documents/picker": "^12.0.0", + "@react-native-vector-icons/fontawesome6": "^12.3.1", "@react-native-vector-icons/material-design-icons": "^12.4.0", "@react-native-vector-icons/material-icons": "^12.4.0", "@react-navigation/bottom-tabs": "^7.9.0", @@ -62,7 +65,7 @@ "@react-native-community/cli": "20.0.2", "@react-native-community/cli-platform-android": "20.0.2", "@react-native-community/cli-platform-ios": "20.0.2", - "@react-native/babel-preset": "0.83.1", + "@react-native/babel-preset": "0.84.0", "@react-native/eslint-config": "0.83.1", "@react-native/metro-config": "0.83.1", "@react-native/typescript-config": "0.83.1", diff --git a/formulus/scripts/generatePlaceholderTokens.js b/formulus/scripts/generatePlaceholderTokens.js new file mode 100644 index 000000000..feaeb73bc --- /dev/null +++ b/formulus/scripts/generatePlaceholderTokens.js @@ -0,0 +1,139 @@ +/** + * Generates the :root CSS block for placeholder_app.html from @ode/tokens. + * Run from formulus root: node scripts/generatePlaceholderTokens.js + * When ODE tokens change, run this script (or prebuild) so the placeholder updates. + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const formulusRoot = path.resolve(__dirname, '..'); + +const TOKENS_PATH = path.join( + formulusRoot, + 'node_modules', + '@ode', + 'tokens', + 'dist', + 'json', + 'tokens.json', +); +const PLACEHOLDER_PATH = path.join( + formulusRoot, + 'assets', + 'webview', + 'placeholder_app.html', +); + +const MARKER_START = '/* ODE_TOKENS_START */'; +const MARKER_END = '/* ODE_TOKENS_END */'; + +function loadTokens() { + if (!fs.existsSync(TOKENS_PATH)) { + console.error( + `[generatePlaceholderTokens] Tokens not found at ${TOKENS_PATH}\n` + + 'Run "npm run build" in packages/tokens (or from repo root) and ensure formulus has @ode/tokens linked.', + ); + process.exit(1); + } + return JSON.parse(fs.readFileSync(TOKENS_PATH, 'utf8')); +} + +function generateRootBlock(t) { + const c = t.color || {}; + const brand = c.brand || {}; + const primary = brand.primary || {}; + const neutral = c.neutral || {}; + const spacing = t.spacing || {}; + const font = t.font || {}; + const size = font.size || {}; + const weight = font.weight || {}; + const family = font.family || {}; + const lineHeight = font.lineHeight || {}; + const border = t.border || {}; + const radius = border.radius || {}; + const width = border.width || {}; + const filter = t.filter || {}; + const blur = filter.blur || {}; + const opacity = t.opacity || {}; + + const opacity70 = opacity['70'] ?? '0.7'; + + return `/* + * ODE design tokens — generated from packages/tokens. Do not edit; run npm run generate:placeholder-tokens to update. + */ + :root { + /* color.brand.primary */ + --color-brand-primary-500: ${primary['500'] ?? '#4f7f4e'}; + --color-brand-primary-400: ${primary['400'] ?? '#6fa46e'}; + /* color.neutral */ + --color-neutral-black: ${neutral.black ?? '#000000'}; + --color-neutral-white: ${neutral.white ?? '#ffffff'}; + --color-neutral-400: ${neutral['400'] ?? '#bdbdbd'}; + --color-neutral-600: ${neutral['600'] ?? '#757575'}; + /* opacity.70 for overlay */ + --opacity-70: ${opacity70}; + --overlay-light: rgba(255, 255, 255, var(--opacity-70)); + --overlay-dark: rgba(0, 0, 0, var(--opacity-70)); + /* spacing */ + --spacing-1: ${spacing['1'] ?? '4px'}; + --spacing-2: ${spacing['2'] ?? '8px'}; + --spacing-3: ${spacing['3'] ?? '12px'}; + --spacing-4: ${spacing['4'] ?? '16px'}; + --spacing-6: ${spacing['6'] ?? '24px'}; + --spacing-8: ${spacing['8'] ?? '32px'}; + --spacing-10: ${spacing['10'] ?? '40px'}; + /* font.size */ + --font-size-xs: ${size.xs ?? '12px'}; + --font-size-sm: ${size.sm ?? '14px'}; + --font-size-base: ${size.base ?? '16px'}; + --font-size-xl: ${size.xl ?? '20px'}; + --font-size-3xl: ${size['3xl'] ?? '32px'}; + /* font.weight */ + --font-weight-regular: ${weight.regular ?? '400'}; + --font-weight-bold: ${weight.bold ?? '700'}; + /* font.family.sans */ + --font-family-sans: ${(family.sans || '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif').replace(/"/g, "'")}; + /* font.lineHeight */ + --line-height-tight: ${lineHeight.tight ?? '1.25'}; + --line-height-normal: ${lineHeight.normal ?? '1.5'}; + /* border.radius.md, border.width.thin */ + --border-radius-md: ${radius.md ?? '8px'}; + --border-width-thin: ${width.thin ?? '1px'}; + /* filter.blur */ + --blur-4: ${blur['4'] ?? '4px'}; + --blur-7: ${blur['7'] ?? '7px'}; + /* content max-width: no ODE token; layout constant */ + --content-max-width: 320px; + }`; +} + +function main() { + const tokens = loadTokens(); + const rootBlock = generateRootBlock(tokens); + + let html = fs.readFileSync(PLACEHOLDER_PATH, 'utf8'); + + if (!html.includes(MARKER_START) || !html.includes(MARKER_END)) { + console.error( + '[generatePlaceholderTokens] Placeholder HTML must contain /* ODE_TOKENS_START */ and /* ODE_TOKENS_END */.', + ); + process.exit(1); + } + + const startIdx = html.indexOf(MARKER_START); + const endIdx = html.indexOf(MARKER_END) + MARKER_END.length; + const before = html.slice(0, startIdx); + const after = html.slice(endIdx); + const newContent = + before + MARKER_START + '\n' + rootBlock + '\n ' + MARKER_END + after; + + fs.writeFileSync(PLACEHOLDER_PATH, newContent, 'utf8'); + console.log( + '[generatePlaceholderTokens] Updated assets/webview/placeholder_app.html from @ode/tokens.', + ); +} + +main(); diff --git a/formulus/scripts/syncNativeVersion.js b/formulus/scripts/syncNativeVersion.js new file mode 100644 index 000000000..d28ecd0f9 --- /dev/null +++ b/formulus/scripts/syncNativeVersion.js @@ -0,0 +1,107 @@ +/** + * Syncs version from package.json to native app manifests. + * + * This script ensures that: + * - Android build.gradle versionName matches package.json.version + * - iOS version fields are kept in sync (if applicable) + * + * Run this before building native apps to prevent version drift. + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const ROOT_DIR = path.resolve(__dirname, '..'); +const PACKAGE_JSON_PATH = path.join(ROOT_DIR, 'package.json'); +const ANDROID_BUILD_GRADLE_PATH = path.join( + ROOT_DIR, + 'android', + 'app', + 'build.gradle', +); + +/** + * Reads version from package.json + */ +function getPackageVersion() { + const packageJsonContent = fs.readFileSync(PACKAGE_JSON_PATH, 'utf-8'); + const packageJson = JSON.parse(packageJsonContent); + if (!packageJson.version) { + throw new Error('package.json must have a "version" field'); + } + return packageJson.version.trim(); +} + +/** + * Updates versionName in Android build.gradle + */ +function updateAndroidVersion(version) { + if (!fs.existsSync(ANDROID_BUILD_GRADLE_PATH)) { + console.warn( + `Android build.gradle not found at ${ANDROID_BUILD_GRADLE_PATH}, skipping Android version update`, + ); + return; + } + + let gradleContent = fs.readFileSync(ANDROID_BUILD_GRADLE_PATH, 'utf-8'); + + // Match versionName = "..." or versionName = '...' + // This regex handles both single and double quotes, and optional whitespace + const versionNameRegex = /versionName\s*=\s*["']([^"']+)["']/; + + if (!versionNameRegex.test(gradleContent)) { + throw new Error( + `Could not find versionName in ${ANDROID_BUILD_GRADLE_PATH}. Expected format: versionName = "1.0.0"`, + ); + } + + const oldVersionMatch = gradleContent.match(versionNameRegex); + const oldVersion = oldVersionMatch ? oldVersionMatch[1] : 'unknown'; + + // Replace versionName with the version from package.json + gradleContent = gradleContent.replace( + versionNameRegex, + `versionName = "${version}"`, + ); + + fs.writeFileSync(ANDROID_BUILD_GRADLE_PATH, gradleContent, 'utf-8'); + console.log( + `✓ Updated Android versionName: ${oldVersion} → ${version} in ${path.relative(ROOT_DIR, ANDROID_BUILD_GRADLE_PATH)}`, + ); +} + +/** + * Main sync function + */ +function syncNativeVersions() { + try { + const version = getPackageVersion(); + console.log( + `Syncing native app versions from package.json (version: ${version})...`, + ); + + updateAndroidVersion(version); + + // Note: iOS version is typically managed via Xcode project settings + // (MARKETING_VERSION and CURRENT_PROJECT_VERSION in project.pbxproj) + // For now, we only sync Android. iOS can be added later if needed. + + console.log('✓ Native version sync complete'); + } catch (error) { + console.error('✗ Failed to sync native versions:', error); + // eslint-disable-next-line no-undef + process.exit(1); + } +} + +// Run if executed directly +// eslint-disable-next-line no-undef +if (import.meta.url === `file://${process.argv[1]}`) { + syncNativeVersions(); +} + +export { syncNativeVersions }; diff --git a/formulus/src/api/synkronus/Auth.ts b/formulus/src/api/synkronus/Auth.ts index 8c4a2b67c..e4adcc133 100644 --- a/formulus/src/api/synkronus/Auth.ts +++ b/formulus/src/api/synkronus/Auth.ts @@ -15,15 +15,33 @@ export interface UserInfo { export interface HttpError extends Error { response?: { status?: number; - data?: { status?: number }; + data?: { + status?: number; + code?: string; + synkronus_version?: string; + }; }; status?: number; statusCode?: number; - body?: { status?: number }; - data?: { status?: number }; + body?: { + status?: number; + code?: string; + synkronus_version?: string; + }; + data?: { + status?: number; + code?: string; + synkronus_version?: string; + }; code?: string | number; } +// Re-export VersionMismatchError for convenience +export { + VersionMismatchError, + isVersionMismatchError, +} from '../../errors/VersionMismatchError'; + const decodeBase64 = (input: string): string => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const atobFn = (globalThis as any).atob as diff --git a/formulus/src/api/synkronus/__tests__/Auth.test.ts b/formulus/src/api/synkronus/__tests__/Auth.test.ts index 7d56a77ff..21380ac3c 100644 --- a/formulus/src/api/synkronus/__tests__/Auth.test.ts +++ b/formulus/src/api/synkronus/__tests__/Auth.test.ts @@ -49,7 +49,12 @@ jest.mock('../index', () => ({ import { jest, describe, test, expect, beforeEach } from '@jest/globals'; import * as Keychain from 'react-native-keychain'; import AsyncStorage from '@react-native-async-storage/async-storage'; -import { autoLogin, isUnauthorizedError } from '../Auth'; +import { + autoLogin, + isUnauthorizedError, + isVersionMismatchError, +} from '../Auth'; +import { VersionMismatchError } from '../../../errors/VersionMismatchError'; import { synkronusApi } from '../index'; describe('Auth - Auto-Login', () => { @@ -106,6 +111,39 @@ describe('Auth - Auto-Login', () => { }); }); + describe('isVersionMismatchError', () => { + test('should detect VersionMismatchError instance', () => { + const error = new VersionMismatchError( + 'Formulus v0.0.1 is not compatible with this server (v1.0.24). Please update the app.', + '1.0.24', + ); + expect(isVersionMismatchError(error)).toBe(true); + expect(error.message).toContain('v1.0.24'); + expect(error.synkronusVersion).toBe('1.0.24'); + }); + + test('should return false for regular Error', () => { + const error = new Error('Some other error'); + expect(isVersionMismatchError(error)).toBe(false); + }); + + test('should return false for 401 error', () => { + const error = { response: { status: 401 } }; + expect(isVersionMismatchError(error)).toBe(false); + }); + + test('should return false for null/undefined', () => { + expect(isVersionMismatchError(null)).toBe(false); + expect(isVersionMismatchError(undefined)).toBe(false); + }); + + test('VersionMismatchError uses default message when none provided', () => { + const error = new VersionMismatchError(); + expect(error.message).toContain('not supported'); + expect(error.synkronusVersion).toBe('unknown'); + }); + }); + describe('autoLogin', () => { const mockCredentials = { username: 'testuser', diff --git a/formulus/src/api/synkronus/client.ts b/formulus/src/api/synkronus/client.ts new file mode 100644 index 000000000..57909fe5d --- /dev/null +++ b/formulus/src/api/synkronus/client.ts @@ -0,0 +1,30 @@ +/** + * Sets up global axios interceptors for Synkronus API client. + * This file should be imported early in the app lifecycle to ensure + * all API calls are intercepted. + */ +import globalAxios from 'axios'; +import { VersionMismatchError } from '../../errors/VersionMismatchError'; + +/** + * Configure axios interceptors for version mismatch handling. + * Detects HTTP 426 (Upgrade Required) responses and converts them + * to VersionMismatchError instances. + */ +export function setupSynkronusClientInterceptors(): void { + // Response interceptor: convert 426 responses to VersionMismatchError + globalAxios.interceptors.response.use( + response => response, + error => { + // Check if this is a 426 Upgrade Required response + if (error?.response?.status === 426) { + const data = error.response.data; + throw new VersionMismatchError( + data?.message, + data?.synkronus_version || 'unknown', + ); + } + return Promise.reject(error); + }, + ); +} diff --git a/formulus/src/api/synkronus/download.ts b/formulus/src/api/synkronus/download.ts new file mode 100644 index 000000000..ddbfe6c10 --- /dev/null +++ b/formulus/src/api/synkronus/download.ts @@ -0,0 +1,41 @@ +/** + * Wrapper for RNFS.downloadFile that automatically includes + * the x-formulus-version header for all Synkronus downloads. + */ +import RNFS from 'react-native-fs'; +import { FORMULUS_VERSION } from '../../version'; + +export interface SynkronusDownloadOptions { + fromUrl: string; + toFile: string; + authToken: string; + background?: boolean; + progressInterval?: number; + progressDivider?: number; + progress?: (res: { + jobId: number; + contentLength: number; + bytesWritten: number; + }) => void; +} + +/** + * Downloads a file from Synkronus server with required headers. + * Automatically includes x-formulus-version header. + */ +export function synkronusDownload(options: SynkronusDownloadOptions): { + promise: Promise; +} { + return RNFS.downloadFile({ + fromUrl: options.fromUrl, + toFile: options.toFile, + headers: { + Authorization: `Bearer ${options.authToken}`, + 'x-formulus-version': FORMULUS_VERSION, + }, + background: options.background ?? true, + progressInterval: options.progressInterval ?? 500, + progressDivider: options.progressDivider, + progress: options.progress, + }); +} diff --git a/formulus/src/api/synkronus/index.ts b/formulus/src/api/synkronus/index.ts index 69d770803..fa59e7d93 100644 --- a/formulus/src/api/synkronus/index.ts +++ b/formulus/src/api/synkronus/index.ts @@ -15,6 +15,7 @@ import { databaseService } from '../../database/DatabaseService'; import randomId from '@nozbe/watermelondb/utils/common/randomId'; import { clientIdService } from '../../services/ClientIdService'; import { unzip } from 'react-native-zip-archive'; +import { synkronusDownload } from './download'; interface DownloadResult { success: boolean; @@ -52,6 +53,11 @@ class SynkronusApi { const token = await AsyncStorage.getItem('@token'); return token || ''; }, + baseOptions: { + headers: { + 'x-formulus-version': FORMULUS_VERSION, + }, + }, }); } @@ -177,10 +183,10 @@ class SynkronusApi { if (await RNFS.exists(tempExtractPath)) await RNFS.unlink(tempExtractPath); // Download the zip - const downloadResult = await RNFS.downloadFile({ + const downloadResult = await synkronusDownload({ fromUrl: zipUrl, toFile: tempZipPath, - headers: { Authorization: `Bearer ${authToken}` }, + authToken, background: true, progressInterval: 500, progress: res => { @@ -549,14 +555,12 @@ class SynkronusApi { } const authToken = this.fastGetToken_cachedToken ?? (await this.fastGetToken()); - const downloadHeaders: { [key: string]: string } = {}; - downloadHeaders.Authorization = `Bearer ${authToken}`; console.debug(`Downloading from: ${url}`); - const result = await RNFS.downloadFile({ + const result = await synkronusDownload({ fromUrl: url, toFile: localFilePath, - headers: downloadHeaders, + authToken, background: true, progressInterval: 500, // fire at most every 500ms if progressCallback is provided progressDivider: progressCallback ? 1 : 100, // fire at most on every percentage change if progressCallback is provided diff --git a/formulus/src/components/BlurredScreenBackground.tsx b/formulus/src/components/BlurredScreenBackground.tsx new file mode 100644 index 000000000..13a1b79c6 --- /dev/null +++ b/formulus/src/components/BlurredScreenBackground.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { + View, + ImageBackground, + StyleSheet, + ImageSourcePropType, + useWindowDimensions, +} from 'react-native'; +import Svg, { Defs, RadialGradient, Stop, Rect } from 'react-native-svg'; +import { colors } from '../theme/colors'; +import { useAppTheme } from '../contexts/AppThemeContext'; +import tokens from '@ode/tokens/dist/react-native/tokens-resolved'; +import welcomeBgLight from '../../assets/images/welcome-bg-light.png'; +import welcomeBgDark from '../../assets/images/welcome-bg-dark.png'; + +type Tokens = { + opacity: Record; + filter: { blur: Record }; +}; + +const t = tokens as Tokens; + +const parsePx = (value: string | undefined): number => + parseInt(String(value ?? '').replace('px', ''), 10) || 0; + +/** Same settings as Welcome/Placeholder: ODE blur and overlay. */ +const backgroundConfig = { + imageVisibility: Number(t.opacity?.['30']) || 0.3, + blurRadiusLight: parsePx(t.filter?.blur?.['4']) || 4, + blurRadiusDark: parsePx(t.filter?.blur?.['7']) || 7, +}; + +/** Dark mode only: subtle edge-darkening overlay to remove bright vignette from asset. */ +const EdgeDarkeningOverlay = () => { + const { width, height } = useWindowDimensions(); + return ( + + + + + + + + + + + + + ); +}; + +interface BlurredScreenBackgroundProps { + children: React.ReactNode; +} + +/** + * Shared blurred background for Welcome, Placeholder, Forms, Observations, + * Sync, About, Help, Settings. Uses welcome-bg-light / welcome-bg-dark with + * ODE blur and overlay; dark mode adds a subtle edge-darkening to remove + * bright vignette from the asset. + */ +const BlurredScreenBackground: React.FC = ({ + children, +}) => { + const { resolvedMode } = useAppTheme(); + const isDark = resolvedMode === 'dark'; + const backgroundImage: ImageSourcePropType = isDark + ? welcomeBgDark + : welcomeBgLight; + const overlayColor = isDark ? colors.neutral.black : colors.neutral.white; + const blurRadius = isDark + ? backgroundConfig.blurRadiusDark + : backgroundConfig.blurRadiusLight; + + return ( + + + + {isDark && } + {children} + + + ); +}; + +const styles = StyleSheet.create({ + root: { + flex: 1, + }, + background: { + ...StyleSheet.absoluteFillObject, + }, + contentSlot: { + flex: 1, + }, +}); + +export default BlurredScreenBackground; diff --git a/formulus/src/components/CustomAppWebView.tsx b/formulus/src/components/CustomAppWebView.tsx index 163cf9379..eaa251e46 100644 --- a/formulus/src/components/CustomAppWebView.tsx +++ b/formulus/src/components/CustomAppWebView.tsx @@ -32,6 +32,12 @@ interface CustomAppWebViewProps { appName?: string; // To identify the source of logs onLoadEndProp?: () => void; // Propagate WebView's onLoadEnd event onCanGoBackChange?: (canGoBack: boolean) => void; // Notify parent when WebView back-navigability changes + /** Called when placeholder posts formulusNavigateToSync (e.g. "Login Now" button). */ + onNavigateToSync?: () => void; + /** Called when placeholder posts formulusNavigateToSettings (Login Now → login/settings screen). */ + onNavigateToSettings?: () => void; + /** When true, WebView and container use transparent background (e.g. for placeholder over BlurredScreenBackground). */ + transparentBackground?: boolean; } const INJECTION_SCRIPT_PATH = @@ -156,86 +162,118 @@ const consoleLogScript = ` const CustomAppWebView = forwardRef< CustomAppWebViewHandle, CustomAppWebViewProps ->(({ appUrl, appName, onLoadEndProp, onCanGoBackChange }, ref) => { - const webViewRef = useRef(null); - const hasLoadedOnceRef = useRef(false); - - const canGoBackRef = useRef(false); - - const onCanGoBackChangeRef = useRef(onCanGoBackChange); - useEffect(() => { - onCanGoBackChangeRef.current = onCanGoBackChange; - }, [onCanGoBackChange]); - - const [injectionScript, setInjectionScript] = - useState(consoleLogScript); - const injectionScriptRef = useRef(consoleLogScript); - const [isScriptReady, setIsScriptReady] = useState(false); - - useEffect(() => { - const loadScript = async () => { - try { - let script = ''; - - if (Platform.OS === 'android') { - // Path A: Use the Android-only asset reader - script = await readFileAssets(INJECTION_SCRIPT_PATH); - } else { - const iosPath = `${MainBundlePath}/${INJECTION_SCRIPT_PATH}`; - script = await readFile(iosPath, 'utf8'); - } +>( + ( + { + appUrl, + appName, + onLoadEndProp, + onCanGoBackChange, + onNavigateToSync, + onNavigateToSettings, + transparentBackground = false, + }, + ref, + ) => { + const webViewRef = useRef(null); + const hasLoadedOnceRef = useRef(false); + + const canGoBackRef = useRef(false); + + const onCanGoBackChangeRef = useRef(onCanGoBackChange); + useEffect(() => { + onCanGoBackChangeRef.current = onCanGoBackChange; + }, [onCanGoBackChange]); + + const onNavigateToSyncRef = useRef(onNavigateToSync); + useEffect(() => { + onNavigateToSyncRef.current = onNavigateToSync; + }, [onNavigateToSync]); + + const onNavigateToSettingsRef = useRef(onNavigateToSettings); + useEffect(() => { + onNavigateToSettingsRef.current = onNavigateToSettings; + }, [onNavigateToSettings]); + + const [injectionScript, setInjectionScript] = + useState(consoleLogScript); + const injectionScriptRef = useRef(consoleLogScript); + const [isScriptReady, setIsScriptReady] = useState(false); + + useEffect(() => { + const loadScript = async () => { + try { + let script = ''; - const fullScript = - consoleLogScript + - '\n' + - hashNavigationTrackingScript + - '\n' + - script; - setInjectionScript(fullScript); - setIsScriptReady(true); - } catch (err) { - // Logic for if the file is missing entirely - console.error('Failed to load injection script with error:', err); - // setIsScriptReady(true); - } - }; - loadScript(); - }, []); - - const messageManager = useMemo(() => { - const manager = new FormulusWebViewMessageManager(webViewRef, appName); - - // Extend the message manager to handle API recovery requests - const originalHandleMessage = manager.handleWebViewMessage; - manager.handleWebViewMessage = event => { - try { - const eventData = JSON.parse(event.nativeEvent.data); - - if (eventData.type === 'hashNavigationStateChange') { - const newCanGoBack = !!eventData.canGoBack; - if (canGoBackRef.current !== newCanGoBack) { - canGoBackRef.current = newCanGoBack; - onCanGoBackChangeRef.current?.(newCanGoBack); + if (Platform.OS === 'android') { + // Path A: Use the Android-only asset reader + script = await readFileAssets(INJECTION_SCRIPT_PATH); + } else { + const iosPath = `${MainBundlePath}/${INJECTION_SCRIPT_PATH}`; + script = await readFile(iosPath, 'utf8'); } - return; + + const fullScript = + consoleLogScript + + '\n' + + hashNavigationTrackingScript + + '\n' + + script; + setInjectionScript(fullScript); + setIsScriptReady(true); + } catch (err) { + // Logic for if the file is missing entirely + console.error('Failed to load injection script with error:', err); + // setIsScriptReady(true); } + }; + loadScript(); + }, []); - // Handle API re-injection requests from WebView - if (eventData.type === 'requestApiReinjection') { - console.log( - `[CustomAppWebView - ${ - appName || 'Default' - }] WebView requested API re-injection`, - ); + const messageManager = useMemo(() => { + const manager = new FormulusWebViewMessageManager(webViewRef, appName); - // Perform immediate re-injection - const latestScript = injectionScriptRef.current; - if ( - webViewRef.current && - latestScript !== consoleLogScript && - hasLoadedOnceRef.current - ) { - const reInjectionWrapper = ` + // Extend the message manager to handle API recovery requests + const originalHandleMessage = manager.handleWebViewMessage; + manager.handleWebViewMessage = event => { + try { + const eventData = JSON.parse(event.nativeEvent.data); + + if (eventData.type === 'hashNavigationStateChange') { + const newCanGoBack = !!eventData.canGoBack; + if (canGoBackRef.current !== newCanGoBack) { + canGoBackRef.current = newCanGoBack; + onCanGoBackChangeRef.current?.(newCanGoBack); + } + return; + } + + if (eventData.type === 'formulusNavigateToSync') { + onNavigateToSyncRef.current?.(); + return; + } + // Handle placeholder "Login Now" → navigate to Settings (login / server credentials / QR) + if (eventData.type === 'formulusNavigateToSettings') { + onNavigateToSettingsRef.current?.(); + return; + } + + // Handle API re-injection requests from WebView + if (eventData.type === 'requestApiReinjection') { + console.log( + `[CustomAppWebView - ${ + appName || 'Default' + }] WebView requested API re-injection`, + ); + + // Perform immediate re-injection + const latestScript = injectionScriptRef.current; + if ( + webViewRef.current && + latestScript !== consoleLogScript && + hasLoadedOnceRef.current + ) { + const reInjectionWrapper = ` (function() { console.debug('[CustomAppWebView/ApiRecovery] Processing re-injection request...'); @@ -252,73 +290,73 @@ const CustomAppWebView = forwardRef< return true; })(); `; - webViewRef.current.injectJavaScript(reInjectionWrapper); + webViewRef.current.injectJavaScript(reInjectionWrapper); + } + return; } - return; + } catch (error: unknown) { + console.error('Error parsing event data:', error); + // If parsing fails, let the original handler deal with it } - } catch (error: unknown) { - console.error('Error parsing event data:', error); - // If parsing fails, let the original handler deal with it - } - // Call the original handler for all other messages - originalHandleMessage.call(manager, event); - }; + // Call the original handler for all other messages + originalHandleMessage.call(manager, event); + }; - return manager; - }, [appName]); + return manager; + }, [appName]); - const handleNavigationStateChange = useCallback( - (navState: WebViewNavigation) => { - const newCanGoBack = navState.canGoBack; - if (canGoBackRef.current !== newCanGoBack) { - canGoBackRef.current = newCanGoBack; - onCanGoBackChange?.(newCanGoBack); - } - }, - [onCanGoBackChange], - ); + const handleNavigationStateChange = useCallback( + (navState: WebViewNavigation) => { + const newCanGoBack = navState.canGoBack; + if (canGoBackRef.current !== newCanGoBack) { + canGoBackRef.current = newCanGoBack; + onCanGoBackChange?.(newCanGoBack); + } + }, + [onCanGoBackChange], + ); - // Expose imperative handle - useImperativeHandle( - ref, - () => ({ - reload: () => webViewRef.current?.reload?.(), - goBack: () => webViewRef.current?.goBack?.(), - goForward: () => webViewRef.current?.goForward?.(), - canGoBack: () => canGoBackRef.current, - injectJavaScript: (script: string) => - webViewRef.current?.injectJavaScript(script), - sendFormInit: (formData: FormInitData) => - messageManager.sendFormInit(formData), - sendAttachmentData: (attachmentData: File) => - messageManager.sendAttachmentData(attachmentData), - }), - [messageManager], - ); - - const handleError = (syntheticEvent: SyntheticEvent) => { - const { nativeEvent } = syntheticEvent; - console.error( - '[CustomAppWebView] WebView error', - nativeEvent, - 'appUrl:', - appUrl, + // Expose imperative handle + useImperativeHandle( + ref, + () => ({ + reload: () => webViewRef.current?.reload?.(), + goBack: () => webViewRef.current?.goBack?.(), + goForward: () => webViewRef.current?.goForward?.(), + canGoBack: () => canGoBackRef.current, + injectJavaScript: (script: string) => + webViewRef.current?.injectJavaScript(script), + sendFormInit: (formData: FormInitData) => + messageManager.sendFormInit(formData), + sendAttachmentData: (attachmentData: File) => + messageManager.sendAttachmentData(attachmentData), + }), + [messageManager], ); - }; - - const isFocused = useIsFocused(); - - useEffect(() => { - // Ensure webViewRef.current and injectionScript (the fully prepared script) are available, and initial load has completed. - if ( - isFocused && - webViewRef.current && - injectionScript !== consoleLogScript && - hasLoadedOnceRef.current - ) { - // Check injectionScript is loaded and initial load done - const reInjectionWrapper = ` + + const handleError = (syntheticEvent: SyntheticEvent) => { + const { nativeEvent } = syntheticEvent; + console.error( + '[CustomAppWebView] WebView error', + nativeEvent, + 'appUrl:', + appUrl, + ); + }; + + const isFocused = useIsFocused(); + + useEffect(() => { + // Ensure webViewRef.current and injectionScript (the fully prepared script) are available, and initial load has completed. + if ( + isFocused && + webViewRef.current && + injectionScript !== consoleLogScript && + hasLoadedOnceRef.current + ) { + // Check injectionScript is loaded and initial load done + const reInjectionWrapper = ` (function() { if (typeof window.formulus === 'undefined' && typeof globalThis.formulus === 'undefined') { console.debug('[CustomAppWebView/FocusEffect] window.formulus is undefined AFTER LOAD. Re-injecting main script content.'); @@ -336,70 +374,77 @@ const CustomAppWebView = forwardRef< return true; // Return true to prevent potential errors in some WebView versions })(); `; - webViewRef.current.injectJavaScript(reInjectionWrapper); - } - }, [isFocused, injectionScript]); // Depend on injectionScript to use the latest version - - // AppState listener to detect when app regains focus and trigger handleReceiveFocus - useEffect(() => { - const handleAppStateChange = (nextAppState: string) => { - if (nextAppState === 'active') { - console.log( - '[CustomAppWebView] App became active, triggering handleReceiveFocus', - ); - // Call handleReceiveFocus on the messageManager when app becomes active - if ( - messageManager && - typeof messageManager.handleReceiveFocus === 'function' - ) { - messageManager.handleReceiveFocus(); - } + webViewRef.current.injectJavaScript(reInjectionWrapper); } - }; + }, [isFocused, injectionScript]); // Depend on injectionScript to use the latest version - const subscription = AppState.addEventListener( - 'change', - handleAppStateChange, - ); + // AppState listener to detect when app regains focus and trigger handleReceiveFocus + useEffect(() => { + const handleAppStateChange = (nextAppState: string) => { + if (nextAppState === 'active') { + console.log( + '[CustomAppWebView] App became active, triggering handleReceiveFocus', + ); + // Call handleReceiveFocus on the messageManager when app becomes active + if ( + messageManager && + typeof messageManager.handleReceiveFocus === 'function' + ) { + messageManager.handleReceiveFocus(); + } + } + }; - return () => { - subscription?.remove(); - }; - }, [messageManager]); + const subscription = AppState.addEventListener( + 'change', + handleAppStateChange, + ); - // const handleWebViewMessage = createFormulusMessageHandler(webViewRef, appName); // Replaced by messageManager - // If appName is undefined, createFormulusMessageHandler will use its default 'WebView' + return () => { + subscription?.remove(); + }; + }, [messageManager]); + + // const handleWebViewMessage = createFormulusMessageHandler(webViewRef, appName); // Replaced by messageManager + // If appName is undefined, createFormulusMessageHandler will use its default 'WebView' + + if (!isScriptReady) { + return ( + + + + ); + } - if (!isScriptReady) { return ( - - - - ); - } - - return ( - - console.debug( - `[CustomAppWebView - ${appName || 'Default'}] Starting to load URL:`, - appUrl, - ) - } - onLoadEnd={() => { - console.debug( - `[CustomAppWebView - ${ - appName || 'Default' - }] Finished loading URL: ${appUrl}`, - ); - if (webViewRef.current) { - // Ensure API is available after load - const ensureApiScript = ` + + console.debug( + `[CustomAppWebView - ${appName || 'Default'}] Starting to load URL:`, + appUrl, + ) + } + onLoadEnd={() => { + console.debug( + `[CustomAppWebView - ${ + appName || 'Default' + }] Finished loading URL: ${appUrl}`, + ); + if (webViewRef.current) { + // Ensure API is available after load + const ensureApiScript = ` (function() { if (typeof window.formulus === 'undefined' && typeof globalThis.formulus !== 'undefined') { window.formulus = globalThis.formulus; @@ -409,43 +454,44 @@ const CustomAppWebView = forwardRef< } })(); `; - webViewRef.current.injectJavaScript(ensureApiScript); - } - hasLoadedOnceRef.current = true; - if (onLoadEndProp) { - onLoadEndProp(); + webViewRef.current.injectJavaScript(ensureApiScript); + } + hasLoadedOnceRef.current = true; + if (onLoadEndProp) { + onLoadEndProp(); + } + }} + onHttpError={syntheticEvent => { + const { nativeEvent } = syntheticEvent; + console.error('CustomWebView HTTP error:', nativeEvent); + }} + injectedJavaScriptBeforeContentLoaded={injectionScript} + javaScriptEnabled={true} + domStorageEnabled={true} + allowFileAccess={true} + allowUniversalAccessFromFileURLs={true} + allowFileAccessFromFileURLs={true} + // iOS requires read access to the directory containing the file, not just the file itself + // For custom apps from DocumentDirectoryPath, allow access to the app directory + // For bundled assets (MainBundlePath), allow access to the bundle root + allowingReadAccessToURL={ + Platform.OS === 'ios' + ? appUrl.includes(MainBundlePath) + ? `file://${MainBundlePath}` + : appUrl.substring(0, appUrl.lastIndexOf('/')) + : undefined } - }} - onHttpError={syntheticEvent => { - const { nativeEvent } = syntheticEvent; - console.error('CustomWebView HTTP error:', nativeEvent); - }} - injectedJavaScriptBeforeContentLoaded={injectionScript} - javaScriptEnabled={true} - domStorageEnabled={true} - allowFileAccess={true} - allowUniversalAccessFromFileURLs={true} - allowFileAccessFromFileURLs={true} - // iOS requires read access to the directory containing the file, not just the file itself - // For custom apps from DocumentDirectoryPath, allow access to the app directory - // For bundled assets (MainBundlePath), allow access to the bundle root - allowingReadAccessToURL={ - Platform.OS === 'ios' - ? appUrl.includes(MainBundlePath) - ? `file://${MainBundlePath}` - : appUrl.substring(0, appUrl.lastIndexOf('/')) - : undefined - } - startInLoadingState={true} - originWhitelist={['*']} - renderLoading={() => ( - - - - )} - /> - ); -}); + startInLoadingState={true} + originWhitelist={['*']} + renderLoading={() => ( + + + + )} + /> + ); + }, +); const styles = StyleSheet.create({ centered: { @@ -453,6 +499,16 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, + webView: { + flex: 1, + }, + webViewTransparent: { + backgroundColor: 'transparent', + }, + webViewContainerTransparent: { + backgroundColor: 'transparent', + flex: 1, + }, }); export default CustomAppWebView; diff --git a/formulus/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx index 231c6ed24..559ae1052 100644 --- a/formulus/src/components/FormplayerModal.tsx +++ b/formulus/src/components/FormplayerModal.tsx @@ -13,9 +13,7 @@ import { TouchableOpacity, Text, Platform, - Alert, ActivityIndicator, - useColorScheme, } from 'react-native'; import CustomAppWebView, { CustomAppWebViewHandle, @@ -32,11 +30,12 @@ import { } from '../webview/FormulusInterfaceDefinition'; import { databaseService } from '../database'; -import { colors } from '../theme/colors'; +import colors, { withAlpha, CONTAINER_ALPHA } from '../theme/colors'; import { FormSpec } from '../services'; // FormService will be imported directly import { ExtensionService } from '../services/ExtensionService'; import RNFS from 'react-native-fs'; import { useAppTheme } from '../contexts/AppThemeContext'; +import { useConfirmModal } from '../contexts/ConfirmModalContext'; import { geolocationService } from '../services/GeolocationService'; interface FormplayerModalProps { @@ -62,11 +61,10 @@ const FormplayerModal = forwardRef( ({ visible, onClose }, ref) => { const webViewRef = useRef(null); const [isSubmitting, setIsSubmitting] = useState(false); - const colorScheme = useColorScheme(); + const { showConfirm } = useConfirmModal(); - // Theme colors from the AppThemeContext — updates automatically when - // the custom app config is loaded or the color scheme changes. - const { themeColors } = useAppTheme(); + // Theme colors & resolved mode from AppThemeContext. + const { themeColors, resolvedMode } = useAppTheme(); // Internal state to track current form and observation data const [currentFormType, setCurrentFormType] = useState(null); @@ -158,24 +156,16 @@ const FormplayerModal = forwardRef( return; } - Alert.alert( - 'Close form?', - 'This will close the current form. Any changes made will not be saved, but will be available as a draft next time you open the form.', - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Close form', - style: 'destructive', - onPress: () => { - performClose(); - }, - }, + showConfirm({ + title: 'Close form?', + message: + 'This will close the current form. Any changes made will not be saved, but will be available as a draft next time you open the form.', + buttons: [ + { text: 'Cancel', variant: 'tertiary', onPress: () => {} }, + { text: 'Close form', variant: 'danger', onPress: performClose }, ], - ); - }, [isClosing, isSubmitting, performClose]); + }); + }, [isClosing, isSubmitting, performClose, showConfirm]); // Removed closeFormplayer event listener - now using direct promise-based submission handling @@ -188,9 +178,14 @@ const FormplayerModal = forwardRef( }; }, []); + // Track WebView ready state + const [webViewReady, setWebViewReady] = useState(false); + // Handle WebView load complete const handleWebViewLoad = () => { - // WebView ready - no action needed + console.log('[FormplayerModal] WebView finished loading'); + setWebViewReady(true); + // WebView is now ready to receive form initialization }; // Initialize a form with the given form type and optional existing data @@ -201,6 +196,13 @@ const FormplayerModal = forwardRef( existingObservationData: Record | null, operationId: string | null, ) => { + // Check if WebView is ready, if not log a warning (retry logic will handle it) + if (!webViewReady) { + console.warn( + '[FormplayerModal] WebView not ready yet, form init will be queued by message handler', + ); + } + // Start GPS acquisition early so the fix is ready at save time geolocationService.preCacheLocation(); @@ -227,8 +229,7 @@ const FormplayerModal = forwardRef( // Forward the custom app's theme colors to the Formplayer WebView so // that form UI elements (buttons, inputs, headers) match the branding. - // `themeColors` comes from useAppTheme() and is always up-to-date. - const isDark = colorScheme === 'dark'; + const isDark = resolvedMode === 'dark'; const formParams = { locale: 'en', @@ -306,10 +307,11 @@ const FormplayerModal = forwardRef( 'FormplayerModal: formType.schema is null/undefined for form:', formType.id, ); - Alert.alert( - 'Form Error', - `Form "${formType.name}" has no schema. The form may not have loaded correctly from storage. Try syncing again.`, - ); + showConfirm({ + title: 'Form Error', + message: `Form "${formType.name}" has no schema. The form may not have loaded correctly from storage. Try syncing again.`, + buttons: [{ text: 'OK', variant: 'primary', onPress: () => {} }], + }); return; } @@ -395,10 +397,12 @@ const FormplayerModal = forwardRef( await webViewRef.current.sendFormInit(formInitData); } catch (error) { console.error('FormplayerModal: Error sending form init data:', error); - Alert.alert( - 'Error', - 'Failed to initialize the form UI. Please close and try again.', - ); + showConfirm({ + title: 'Error', + message: + 'Failed to initialize the form UI. Please close and try again.', + buttons: [{ text: 'OK', variant: 'primary', onPress: () => {} }], + }); } }; @@ -465,15 +469,20 @@ const FormplayerModal = forwardRef( const successMessage = currentObservationId ? 'Observation updated successfully!' : 'Form submitted successfully!'; - Alert.alert('Success', successMessage, [ - { - text: 'OK', - onPress: () => { - setIsSubmitting(false); - onClose(); + showConfirm({ + title: 'Success', + message: successMessage, + buttons: [ + { + text: 'OK', + variant: 'primary', + onPress: () => { + setIsSubmitting(false); + onClose(); + }, }, - }, - ]); + ], + }); return resultObservationId; } catch (error) { @@ -494,11 +503,15 @@ const FormplayerModal = forwardRef( resolveFormOperationByType(formType, errorResult); } - Alert.alert('Error', 'Failed to save your form. Please try again.'); + showConfirm({ + title: 'Error', + message: 'Failed to save your form. Please try again.', + buttons: [{ text: 'OK', variant: 'primary', onPress: () => {} }], + }); throw error; } }, - [currentObservationId, currentOperationId, onClose], + [currentObservationId, currentOperationId, onClose, showConfirm], ); // Register/unregister modal with message handlers and reset form state @@ -518,6 +531,7 @@ const FormplayerModal = forwardRef( setCurrentObservationData(null); setIsClosing(false); // Reset closing state when modal is fully closed setFormSubmitted(false); // Reset submission flag + setWebViewReady(false); // Reset WebView ready state }, 300); // Small delay to ensure modal is fully closed } }, [visible, handleSubmission]); @@ -533,7 +547,17 @@ const FormplayerModal = forwardRef( presentationStyle="fullScreen" statusBarTranslucent={false}> + style={[ + styles.container, + { + backgroundColor: withAlpha( + themeColors.surface as string, + CONTAINER_ALPHA, + ), + borderWidth: 1, + borderColor: themeColors.divider as string, + }, + ]}> = ROLE_LEVELS[minRole]; }; +const DIVIDER_HEIGHT = 1; + +/** Same fade as the bottom nav top line: line color with opacity 0 at ends, 1 in the middle. */ +const MenuDivider = ({ color }: { color: string }) => { + const gradientId = React.useId().replace(/:/g, ''); + return ( + + + + + + + + + + + + + + ); +}; + const MenuDrawer: React.FC = ({ visible, onClose, @@ -52,10 +95,34 @@ const MenuDrawer: React.FC = ({ onLogout, allowClose = true, }) => { + const { themeColors, themeMode, setThemeMode, resolvedMode } = useAppTheme(); + const isDark = resolvedMode === 'dark'; + const textColor = isDark + ? (themeColors.onSurface as string) + : (colors.neutral[900] as string); + const odeOpacity = (tokens as { opacity?: Record }).opacity; + const dividerOpacityDark = + odeOpacity?.['10'] != null ? Number(odeOpacity['10']) : 0.1; + const themeChipBorderOpacityDark = + odeOpacity?.['50'] != null ? Number(odeOpacity['50']) : 0.5; + const sectionDividerColor = isDark + ? withAlpha(colors.neutral.white as string, dividerOpacityDark) + : (themeColors.divider as string); + const menuModalBorderColor = sectionDividerColor; + const themeChipBorderColorDark = isDark + ? withAlpha(colors.neutral.white as string, themeChipBorderOpacityDark) + : undefined; + const cardOuterBg = isDark + ? withAlpha(themeColors.surface as string, CONTAINER_ALPHA) + : withAlpha(colors.neutral[900] as string, 0.04); + const cardInnerBg = isDark + ? withAlpha(themeColors.surface as string, CONTAINER_ALPHA) + : withAlpha(colors.neutral[900] as string, 0.02); const [userInfo, setUserInfo] = useState(null); - const insets = useSafeAreaInsets(); - const TAB_BAR_HEIGHT = 60; - const bottomPadding = TAB_BAR_HEIGHT + insets.bottom; + const [appSettingsOpen, setAppSettingsOpen] = useState(true); + // Backdrop covers the full height; bottom nav is rendered above this layer + // by the tab navigator, so its buttons remain clickable. + const bottomPadding = 0; useEffect(() => { if (visible) { @@ -64,11 +131,6 @@ const MenuDrawer: React.FC = ({ }, [visible]); const menuItems: MenuItem[] = [ - { - icon: 'cog', - label: 'App Settings', - screen: 'Settings', - }, { icon: 'information', label: 'About', @@ -97,107 +159,267 @@ const MenuDrawer: React.FC = ({ } }; + if (!visible) { + return null; + } + return ( - - - {allowClose && ( - - )} - - - - Menu - {allowClose && ( - - - - )} - + + {allowClose && ( + + )} + + + + Menu + {allowClose && ( + + + + )} + + - {/* User Info Section */} - {userInfo ? ( - - - + {/* User Info Section */} + {userInfo ? ( + <> + + + - {userInfo.username} - {userInfo.role} + + {userInfo.role === 'admin' + ? 'Admin' + : userInfo.role === 'read-write' + ? 'Read-write' + : 'Read-only'} + - ) : ( - - + + + ) : ( + <> + + - Not logged in - Go to Settings to login + + Not logged in + + + Click the Login at the bottom + - )} + + + )} + + + {/* App Settings dropdown with Themes section, divided by thin lines */} + + + setAppSettingsOpen(open => !open)}> + + + + App Settings + + + + + {appSettingsOpen && ( + + + + Themes + + + {(['system', 'dark', 'light'] as ThemeMode[]).map( + mode => ( + setThemeMode(mode)}> + + {mode === 'system' + ? 'System' + : mode === 'dark' + ? 'Dark' + : 'Light'} + + + ), + )} + + + + )} + + + - - {visibleItems.map((item, index) => ( + {visibleItems.map((item, index) => ( + onNavigate(item.screen)}> - - {item.label} + + + {item.label} + - ))} - + + + ))} + - {userInfo && ( - - - - + title={syncState.isActive ? 'Syncing...' : 'Sync Data'} + onPress={handleSync} + disabled={syncState.isActive}> + {isSyncButtonActive ? ( + + ) : ( + + )} + + {isSyncButtonActive ? 'Syncing...' : 'Sync Data'} + + - {!syncState.isActive && updateAvailable && ( - Update available - )} + - {!syncState.isActive && !updateAvailable && !isAdmin && ( - No updates available - )} - - - + {!syncState.isActive && updateAvailable && ( + + Update available + + )} + + {!syncState.isActive && !updateAvailable && !isAdmin && ( + + No updates available + + )} + + + + ); }; const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: colors.neutral[50], }, header: { - padding: 16, - backgroundColor: colors.neutral.white, - borderBottomWidth: 1, - borderBottomColor: colors.neutral[200], + marginHorizontal: odeSpacing.sm, + padding: odeSpacing.md, + borderBottomWidth: odeBorderWidth.hairline, + borderBottomLeftRadius: odeRadius.card, + borderBottomRightRadius: odeRadius.card, + overflow: 'hidden', + alignItems: 'center', }, title: { - fontSize: 28, + fontSize: odeTypography.screenTitle, fontWeight: 'bold', - color: colors.neutral[900], - marginBottom: 4, + marginBottom: odeSpacing.xs, + textAlign: 'center', }, subtitle: { - fontSize: 14, - color: colors.neutral[600], + fontSize: odeTypography.bodySm, + textAlign: 'center', + }, + scrollTransparent: { + backgroundColor: 'transparent', }, scrollContent: { - padding: 16, - paddingBottom: 32, + padding: odeSpacing.md, + paddingBottom: odeSpacing.xl, + }, + card: { + borderRadius: odeRadius.card, + marginBottom: odeSpacing.md, + borderWidth: odeBorderWidth.hairline, + padding: odeSpacing.md, + }, + cardInner: { + borderRadius: odeRadius.inner, + overflow: 'hidden', + padding: odeSpacing.sm, }, statusCardsContainer: { flexDirection: 'row', - gap: 12, - marginBottom: 16, + gap: odeSpacing.sm, + marginBottom: odeSpacing.md, }, statusCard: { flex: 1, - backgroundColor: colors.neutral.white, - borderRadius: 12, - padding: 16, - shadowColor: colors.neutral.black, - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - // statusCardClickable styles are now applied inline via themeColors + }, statusCardHeader: { flexDirection: 'row', alignItems: 'center', - gap: 8, - marginBottom: 8, + gap: odeSpacing.xs, + marginBottom: odeSpacing.xs, }, statusCardTitle: { - fontSize: 12, + fontSize: odeTypography.caption, fontWeight: '500', - color: colors.neutral[600], textTransform: 'uppercase', }, statusCardValue: { - fontSize: 16, + fontSize: odeTypography.body, fontWeight: '600', - color: colors.neutral[900], }, statusCardSubtext: { - fontSize: 12, - color: colors.neutral[500], - marginTop: 4, + fontSize: odeTypography.caption, + marginTop: odeSpacing.xxs, }, pendingSection: { - backgroundColor: colors.neutral.white, - borderRadius: 12, - padding: 16, - marginBottom: 16, - shadowColor: colors.neutral.black, - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, + marginBottom: odeSpacing.md, }, sectionTitle: { - fontSize: 16, + fontSize: odeTypography.sectionTitle, fontWeight: '600', - color: colors.neutral[900], - marginBottom: 12, + marginBottom: odeSpacing.sm, }, pendingItem: { flexDirection: 'row', alignItems: 'center', - gap: 12, - paddingVertical: 8, + gap: odeSpacing.sm, + paddingVertical: odeSpacing.xs, }, pendingItemContent: { flex: 1, }, pendingItemLabel: { - fontSize: 14, - color: colors.neutral[600], - marginBottom: 2, + fontSize: odeTypography.bodySm, + marginBottom: odeSpacing.xxs, }, pendingItemValue: { - fontSize: 14, + fontSize: odeTypography.bodySm, fontWeight: '600', - color: colors.neutral[900], }, versionCard: { - backgroundColor: colors.neutral.white, - borderRadius: 12, - padding: 16, - marginBottom: 16, - shadowColor: colors.neutral.black, - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, + marginBottom: odeSpacing.md, }, versionRow: { flexDirection: 'row', @@ -723,32 +940,28 @@ const styles = StyleSheet.create({ alignItems: 'center', }, versionLabel: { - fontSize: 14, + fontSize: odeTypography.bodySm, fontWeight: '500', - color: colors.neutral[600], }, versionValues: { flexDirection: 'row', alignItems: 'center', - gap: 12, + gap: odeSpacing.sm, }, versionItem: { alignItems: 'flex-end', }, versionItemLabel: { - fontSize: 11, - color: colors.neutral[500], - marginBottom: 2, + fontSize: odeTypography.caption, + marginBottom: odeSpacing.xxs, }, versionItemValue: { - fontSize: 14, + fontSize: odeTypography.bodySm, fontWeight: '600', - color: colors.neutral[900], }, versionDivider: { width: 1, height: 20, - backgroundColor: colors.neutral[200], }, updateBadge: { flexDirection: 'row', @@ -757,86 +970,79 @@ const styles = StyleSheet.create({ marginTop: 12, paddingTop: 12, borderTopWidth: 1, - borderTopColor: colors.neutral[200], }, updateBadgeText: { fontSize: 12, - color: colors.semantic.success[500], + color: colors.semantic.success[500] as unknown as string, fontWeight: '500', }, progressCard: { - // bg and border colors are overridden inline via themeColors - borderRadius: 12, - padding: 16, - marginBottom: 16, - borderLeftWidth: 4, + marginBottom: odeSpacing.md, }, progressHeader: { flexDirection: 'row', alignItems: 'center', - gap: 8, - marginBottom: 12, + justifyContent: 'center', + gap: odeSpacing.xs, + marginBottom: odeSpacing.sm, }, progressTitle: { - fontSize: 16, + fontSize: odeTypography.sectionTitle, fontWeight: '600', - // color is applied inline via themeColors.primary + textAlign: 'center', }, progressBar: { height: 8, - // backgroundColor is applied inline via themeColors borderRadius: 4, marginBottom: 8, overflow: 'hidden', }, progressFill: { height: '100%', - // backgroundColor is applied inline via themeColors.primary borderRadius: 4, }, progressText: { - fontSize: 12, - color: colors.neutral[600], + fontSize: odeTypography.caption, textAlign: 'center', marginBottom: 12, }, errorCard: { - backgroundColor: colors.semantic.error[50], - borderRadius: 12, - padding: 16, - marginBottom: 16, - borderLeftWidth: 4, - borderLeftColor: colors.semantic.error[500], + marginBottom: odeSpacing.md, }, errorHeader: { flexDirection: 'row', alignItems: 'center', - gap: 8, - marginBottom: 8, + justifyContent: 'center', + gap: odeSpacing.xs, + marginBottom: odeSpacing.sm, }, errorTitle: { - fontSize: 16, + fontSize: odeTypography.sectionTitle, fontWeight: '600', - color: colors.semantic.error[500], + textAlign: 'center', + color: colors.semantic.error[500] as unknown as string, }, errorText: { - fontSize: 14, - color: colors.semantic.error[600], - marginBottom: 12, + fontSize: odeTypography.bodySm, + textAlign: 'center', + marginBottom: odeSpacing.sm, }, actionsSection: { gap: 12, }, + actionButtonText: { + fontSize: odeTypography.body, + fontWeight: '600', + }, + secondaryButtonText: {}, hintText: { fontSize: 12, - color: colors.neutral[500], textAlign: 'center', fontStyle: 'italic', marginTop: 4, }, updateNotification: { fontSize: 12, - color: colors.semantic.warning[600], textAlign: 'center', marginTop: 4, }, diff --git a/formulus/src/screens/WelcomeScreen.tsx b/formulus/src/screens/WelcomeScreen.tsx index 949d6f092..76b94c66f 100644 --- a/formulus/src/screens/WelcomeScreen.tsx +++ b/formulus/src/screens/WelcomeScreen.tsx @@ -6,78 +6,168 @@ import { StackNavigationProp } from '@react-navigation/stack'; import { MainAppStackParamList } from '../types/NavigationTypes'; import { colors } from '../theme/colors'; import { Button } from '../components/common'; +import tokens from '@ode/tokens/dist/react-native/tokens-resolved'; +import BlurredScreenBackground from '../components/BlurredScreenBackground'; +import { useAppTheme } from '../contexts/AppThemeContext'; import logo from '../../assets/images/logo.png'; type WelcomeScreenNavigationProp = StackNavigationProp; +type Tokens = { + border: { width: { thin: string }; radius: { full: string } }; + spacing: Record; + font: { + size: Record; + weight: { regular: string; bold: string }; + }; + logo: { xl: string }; +}; + +const t = tokens as Tokens; + +const parsePx = (value: string | undefined): number => + parseInt(String(value ?? '').replace('px', ''), 10) || 0; + +const ode = { + borderWidthThin: parsePx(t.border?.width?.thin) || 1, + radiusFull: parsePx(t.border?.radius?.full) || 9999, + logoSize: parsePx(t.logo?.xl) || 200, + spacing: { + _1: parsePx(t.spacing?.['1']) || 4, + _2: parsePx(t.spacing?.['2']) || 8, + _3: parsePx(t.spacing?.['3']) || 12, + _8: parsePx(t.spacing?.['8']) || 32, + _10: parsePx(t.spacing?.['10']) || 40, + }, + font: { + sizeBase: parsePx(t.font?.size?.base) || 16, + sizeXl: parsePx(t.font?.size?.xl) || 20, + size3xl: parsePx(t.font?.size?.['3xl']) || 32, + weightRegular: (t.font?.weight?.regular ?? '400') as '400', + weightBold: (t.font?.weight?.bold ?? '700') as '700', + }, +}; + const WelcomeScreen = () => { const navigation = useNavigation(); + const { resolvedMode } = useAppTheme(); + const isDark = resolvedMode === 'dark'; const handleGetStarted = () => { - // Use reset instead of navigate so that "Welcome" is removed from the - // stack history. This prevents the hardware back button from returning - // the user to the Welcome screen after they've configured the server. navigation.reset({ index: 0, routes: [{ name: 'MainApp' }], }); }; + const odeGreen = colors.brand.primary[500]; + const logoBorderColor = isDark ? colors.brand.primary[400] : odeGreen; + const textPrimary = isDark ? colors.neutral.white : colors.neutral.black; + const textSecondary = isDark ? colors.neutral[400] : colors.neutral[600]; + return ( - - - - Welcome to Formulus - - Configure your server to get started - - -