Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ assignees: ''
---

## Component

<!-- Which component is affected? -->

- [ ] formulus (React Native mobile app)
- [ ] formulus-formplayer (React web app)
- [ ] synkronus (Go backend server)
Expand All @@ -16,39 +18,47 @@ assignees: ''
- [ ] Other (please specify)

## User Story

<!-- Optional: Help us understand the context and what you're trying to achieve -->
<!-- Format: As a [user type], I want to [goal] so that [benefit] -->
<!-- Example: As a developer, I want to submit a form so that my data is saved correctly -->

## Description

<!-- A clear and concise description of what the bug is -->

## Details

<!-- Optional: Additional structured details about the bug -->
<!-- Include information such as: -->
<!-- - When does this bug occur? (always, sometimes, specific conditions) -->
<!-- - What triggers the bug? -->
<!-- - Any patterns or related issues? -->

## Steps to Reproduce
1.
2.
3.

1.
2.
3.

## Expected Behavior

<!-- What you expected to happen -->

## Actual Behavior

<!-- What actually happened -->

## Acceptance Criteria

<!-- Optional: Define how to verify this bug is fixed and test the solution -->
<!-- Example: -->
<!-- - [ ] The form submits successfully without errors -->
<!-- - [ ] Error message is displayed when validation fails -->
<!-- - [ ] The bug can be reproduced using the steps above -->

## Environment

<!-- Please provide relevant environment information -->

**OS:** <!-- e.g., macOS 14.0, Ubuntu 22.04, Windows 11 -->
Expand All @@ -58,9 +68,9 @@ assignees: ''
**Component version/branch:** <!-- e.g., main, v1.0.0 -->

## Additional Context

<!-- Add any other context, screenshots, error messages, or logs that might help -->

```
<!-- Paste relevant error messages or logs here -->
```

14 changes: 5 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,15 @@ jobs:
cache-dependency-path: formulus/package-lock.json

- name: Install dependencies
run: npm ci

- name: Run Prettier check
run: npm run format:check
continue-on-error: false
run: npm install

- name: Run linter
run: npm run lint
continue-on-error: false

- name: Run tests
run: npm test -- --coverage --watchAll=false
continue-on-error: true # We want to continue even if tests fail since tests are not set yet
## Ignoring formulus testing for Q2
# - name: Run tests
# run: npm test -- --coverage --watchAll=false
# continue-on-error: true # We want to continue even if tests fail since tests are not set yet

# Lint and test for formulus-formplayer (React Web)
formulus-formplayer:
Expand Down
8 changes: 0 additions & 8 deletions formulus/.eslintignore

This file was deleted.

4 changes: 0 additions & 4 deletions formulus/.eslintrc.js

This file was deleted.

7 changes: 0 additions & 7 deletions formulus/.prettierrc.js

This file was deleted.

54 changes: 34 additions & 20 deletions formulus/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import React, {useEffect, useState} from 'react';
import {NavigationContainer, DefaultTheme} from '@react-navigation/native';
import {StatusBar} from 'react-native';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import React, { useEffect, useState } from 'react';
import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
import { StatusBar } 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 {appEvents} from './src/webview/FormulusMessageHandlers.ts';
import { FormService } from './src/services/FormService';
import { SyncProvider } from './src/contexts/SyncContext';
import { appEvents, Listener } from './src/webview/FormulusMessageHandlers.ts';
import FormplayerModal, {
FormplayerModalHandle,
} from './src/components/FormplayerModal';
import QRScannerModal from './src/components/QRScannerModal';
import SignatureCaptureModal from './src/components/SignatureCaptureModal';
import MainAppNavigator from './src/navigation/MainAppNavigator';
import { FormInitData } from './src/webview/FormulusInterfaceDefinition.ts';

const LightNavigationTheme = {
...DefaultTheme,
Expand All @@ -31,13 +32,13 @@ function App(): React.JSX.Element {
const [qrScannerVisible, setQrScannerVisible] = useState(false);
const [qrScannerData, setQrScannerData] = useState<{
fieldId: string;
onResult: (result: any) => void;
onResult: (result: unknown) => void;
} | null>(null);

const [signatureCaptureVisible, setSignatureCaptureVisible] = useState(false);
const [signatureCaptureData, setSignatureCaptureData] = useState<{
fieldId: string;
onResult: (result: any) => void;
onResult: (result: unknown) => void;
} | null>(null);

const [formplayerVisible, setFormplayerVisible] = useState(false);
Expand All @@ -53,29 +54,33 @@ function App(): React.JSX.Element {

const handleOpenQRScanner = (data: {
fieldId: string;
onResult: (result: any) => void;
onResult: (result: unknown) => void;
}) => {
setQrScannerData(data);
setQrScannerVisible(true);
};

const handleOpenSignatureCapture = (data: {
fieldId: string;
onResult: (result: any) => void;
onResult: (result: unknown) => void;
}) => {
setSignatureCaptureData(data);
setSignatureCaptureVisible(true);
};

appEvents.addListener('openQRScanner', handleOpenQRScanner);
appEvents.addListener('openSignatureCapture', handleOpenSignatureCapture);
appEvents.addListener('openQRScanner', handleOpenQRScanner as Listener);
appEvents.addListener(
'openSignatureCapture',
handleOpenSignatureCapture as Listener,
);

const handleOpenFormplayer = async (config: any) => {
const handleOpenFormplayer = async (config: FormInitData) => {
if (formplayerVisibleRef.current) {
return;
}

const {formType, observationId, params, savedData, operationId} = config;
const { formType, observationId, params, savedData, operationId } =
config;
formplayerVisibleRef.current = true;
setFormplayerVisible(true);

Expand Down Expand Up @@ -105,16 +110,25 @@ function App(): React.JSX.Element {
setFormplayerVisible(false);
};

appEvents.addListener('openFormplayerRequested', handleOpenFormplayer);
appEvents.addListener(
'openFormplayerRequested',
handleOpenFormplayer as Listener,
);
appEvents.addListener('closeFormplayer', handleCloseFormplayer);

return () => {
appEvents.removeListener('openQRScanner', handleOpenQRScanner);
appEvents.removeListener(
'openQRScanner',
handleOpenQRScanner as Listener,
);
appEvents.removeListener(
'openSignatureCapture',
handleOpenSignatureCapture,
handleOpenSignatureCapture as Listener,
);
appEvents.removeListener(
'openFormplayerRequested',
handleOpenFormplayer as Listener,
);
appEvents.removeListener('openFormplayerRequested', handleOpenFormplayer);
appEvents.removeListener('closeFormplayer', handleCloseFormplayer);
};
}, []);
Expand Down Expand Up @@ -152,7 +166,7 @@ function App(): React.JSX.Element {
setSignatureCaptureData(null);
}}
fieldId={signatureCaptureData?.fieldId || ''}
onSignatureCapture={(result: any) => {
onSignatureCapture={(result: unknown) => {
signatureCaptureData?.onResult?.(result);
}}
/>
Expand Down
3 changes: 0 additions & 3 deletions formulus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ The key principle is **decoupling metadata sync (observations) from binary paylo
We separate sync into two distinct phases:

- **Phase 1: Observation data sync**

- Uses the existing `/sync/pull` and `/sync/push` endpoints.
- Syncs only the observation records as JSON (including references to attachment IDs in their `data`).

Expand Down Expand Up @@ -191,7 +190,6 @@ This table is **client-local only**; the server remains agnostic about the clien
### Upload flow

- When a new observation is saved locally and references a new attachment:

- Insert a row in the attachment tracking table with `direction = 'upload'` and `synced = false`.

- The client attachment uploader runs periodically:
Expand All @@ -208,7 +206,6 @@ This table is **client-local only**; the server remains agnostic about the clien
- After completing `/sync/pull`, the client receives new or updated observations.
- It parses the `data` field of those records to extract referenced attachment IDs.
- For each attachment ID:

- Checks if it's already present locally.
- If missing, inserts into the tracking table with `direction = 'download'` and `synced = false`.

Expand Down
8 changes: 4 additions & 4 deletions formulus/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
*/

import React from 'react';
import {render} from '@testing-library/react-native';
import {jest, describe, test, expect} from '@jest/globals';
import { render } from '@testing-library/react-native';
import { View } from 'react-native';
import { jest, describe, test, expect } from '@jest/globals';

// Mock the App component instead of trying to render the real one
// This avoids issues with native modules and database initialization
jest.mock('../App', () => {
const {View} = require('react-native');
return function MockedApp() {
return <View testID="mocked-app" />;
};
Expand All @@ -24,7 +24,7 @@ describe('App', () => {
});

test('renders correctly with mocked implementation', () => {
const {getByTestId} = render(<App />);
const { getByTestId } = render(<App />);
// Our mock returns null, so we shouldn't find any elements
expect(getByTestId('mocked-app')).toBeTruthy();
});
Expand Down
1 change: 0 additions & 1 deletion formulus/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"

/**
* This is the configuration block to customize your React Native Android app.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,25 @@ import android.webkit.WebView
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import org.opendataensemble.formulus.UserAppPackage

class MainApplication : Application(), ReactApplication {

override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(UserAppPackage())
}

override fun getJSMainModuleName(): String = "index"

override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}

override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList = PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here
add(UserAppPackage())
},
)
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes officially retires the legacy ReactNativeHost (The Bridge) in favor of the new ReactHost

Key Changes:
Bridgeless by Default: Enabled the modern Bridgeless runtime using getDefaultReactHost.

Switched to loadReactNative(this), which simplifies the initialization of native components and architecture flags.

Re-registered UserAppPackage within the new reactHost lazy initializer to ensure compatibility via the New Architecture Interop Layer.


override fun onCreate() {
super.onCreate()
// Enable Chrome DevTools debugging for all WebViews in this app
WebView.setWebContentsDebuggingEnabled(true)
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
loadReactNative(this)
}
}
}
8 changes: 4 additions & 4 deletions formulus/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
buildscript {
ext {
buildToolsVersion = "35.0.0"
buildToolsVersion = "36.0.0"
minSdkVersion = 24
compileSdkVersion = 35
targetSdkVersion = 35
compileSdkVersion = 36
targetSdkVersion = 36
ndkVersion = "27.1.12297006"
kotlinVersion = "2.0.21"
kotlinVersion = "2.1.20"
}
repositories {
google()
Expand Down
7 changes: 6 additions & 1 deletion formulus/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,17 @@ reactNativeArchitectures=arm64-v8a
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
newArchEnabled=true

# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true

# Use this property to enable edge-to-edge display support.
# This allows your app to draw behind system bars for an immersive UI.
# Note: Only works with ReactActivity and should not be used with custom Activity.
edgeToEdgeEnabled=false

# Gradle performance optimizations
org.gradle.caching=true
org.gradle.configureondemand=true
Expand Down
Binary file modified formulus/android/gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
Loading