Skip to content
Merged
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
14 changes: 11 additions & 3 deletions formulus/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {
DarkTheme,
} from '@react-navigation/native';
import { StatusBar, Alert, View, ActivityIndicator } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import {
SafeAreaProvider,
initialWindowMetrics,
} from 'react-native-safe-area-context';
import { I18nextProvider } from 'react-i18next';
import 'react-native-url-polyfill/auto';
import { FormService } from './src/services/FormService';
Expand Down Expand Up @@ -323,9 +326,14 @@ function App(): React.JSX.Element {
void initFormulusI18n().then(() => setI18nReady(true));
}, []);

// initialMetrics seeds insets synchronously. Without it, iOS + RN 0.83
// white-screens: Fabric mounts RNCSafeAreaProviderComponentView with its
// final frame, UIKit never runs another layout pass on it, so the native
// onInsetsChange event is never emitted and SafeAreaProvider renders null
// forever (upstream: AppAndFlow/react-native-safe-area-context).
if (!i18nReady) {
return (
<SafeAreaProvider>
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<View
style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
Expand All @@ -336,7 +344,7 @@ function App(): React.JSX.Element {

return (
<I18nextProvider i18n={i18n}>
<SafeAreaProvider>
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<SyncProvider>
<AppThemeProvider>
<ConfirmModalProvider>
Expand Down
112 changes: 112 additions & 0 deletions formulus/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.9)
activesupport (6.1.7.10)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
benchmark (0.5.0)
bigdecimal (4.1.2)
claide (1.1.0)
cocoapods (1.15.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.15.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.6.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.23.0, < 2.0)
cocoapods-core (1.15.2)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (2.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.3.7)
escape (0.0.4)
ethon (0.18.0)
ffi (>= 1.15.0)
logger
ffi (1.17.4)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
httpclient (2.9.0)
mutex_m
i18n (1.14.8)
concurrent-ruby (~> 1.0)
json (2.7.6)
logger (1.7.0)
minitest (5.25.4)
molinillo (0.8.0)
mutex_m (0.3.0)
nanaimo (0.3.0)
nap (1.1.0)
netrc (0.11.0)
public_suffix (4.0.7)
rexml (3.4.4)
ruby-macho (2.5.1)
typhoeus (1.6.0)
ethon (>= 0.18.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
xcodeproj (1.25.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (>= 3.3.6, < 4.0)
zeitwerk (2.6.18)

PLATFORMS
ruby

DEPENDENCIES
activesupport (>= 6.1.7.5, != 7.1.0)
benchmark
bigdecimal
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
concurrent-ruby (< 1.3.8)
logger
mutex_m
xcodeproj (< 1.26.0)

RUBY VERSION
ruby 2.6.10p210

BUNDLED WITH
1.17.2
158 changes: 158 additions & 0 deletions formulus/assets/webview/formulus-load.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* Formulus Load Script
*
* This is a standalone script that client code must include to access the Formulus API.
* It handles complete injection failure and recovery.
*
* Usage:
* <script src="formulus-load.js"></script>
* <script>
* const api = await getFormulus();
* const version = await api.getVersion();
* console.log(version);
* </script>
*/

(function () {
'use strict';

// Prevent multiple inclusions
if (window.getFormulus) {
return;
}

/**
* The ONLY function client code should use to access Formulus API
* This function is completely self-contained and can recover from any injection failure
*/
window.getFormulus = function () {
return new Promise((resolve, reject) => {
console.log('getFormulus: Starting API load...');

// Step 1: Check if we already have a working API from injection
if (checkExistingAPI()) {
console.log('getFormulus: Found existing working API');
resolve(getExistingAPI());
return;
}

// Step 2: No API found, initiate recovery
console.log('getFormulus: No API found, initiating recovery...');
initiateRecovery();

function checkExistingAPI() {
const api = window.formulus || window.globalThis?.formulus;
return (
api && typeof api === 'object' && typeof api.getVersion === 'function'
);
}

function getExistingAPI() {
const api = window.formulus || window.globalThis?.formulus;
ensureGetObservationsByQuery(api);
return api;
}

function ensureGetObservationsByQuery(api) {
if (!api) return;
// Always replace: injected API may have buggy getObservationsByQuery that delegates to getObservations (drops whereClause)
api.getObservationsByQuery = function (options) {
return new Promise(function (resolve, reject) {
const messageId =
'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000);
const callback = function (event) {
try {
var data =
typeof event.data === 'string'
? JSON.parse(event.data)
: event.data;
if (
data.type === 'getObservationsByQuery_response' &&
data.messageId === messageId
) {
window.removeEventListener('message', callback);
if (data.error) reject(new Error(data.error));
else resolve(data.result);
}
} catch (e) {
window.removeEventListener('message', callback);
reject(e);
}
};
window.addEventListener('message', callback);
window.ReactNativeWebView.postMessage(
JSON.stringify({
type: 'getObservationsByQuery',
messageId: messageId,
formType: options.formType,
isDraft: options.isDraft,
includeDeleted: options.includeDeleted,
filter: options.filter,
whereClause: options.whereClause,
}),
);
});
};
}

function initiateRecovery() {
// Request re-injection from React Native host
if (window.ReactNativeWebView) {
console.log('getFormulus: Requesting API re-injection from host...');
window.ReactNativeWebView.postMessage(
JSON.stringify({
type: 'requestApiReinjection',
timestamp: Date.now(),
reason: 'api_load_recovery',
}),
);
} else {
console.warn(
'getFormulus: ReactNativeWebView not available, cannot request re-injection',
);
}

// Wait for re-injection to complete
let attempts = 0;
const maxAttempts = 50; // 5 seconds with 100ms intervals

const checkForRecovery = () => {
attempts++;

console.log(
`getFormulus: Recovery attempt ${attempts}/${maxAttempts}`,
);

// Check if we now have a working API
if (checkExistingAPI()) {
console.log('getFormulus: API recovery successful');
resolve(getExistingAPI());
return;
}

if (attempts >= maxAttempts) {
const errorMsg =
'Formulus API load failed: No API available after maximum recovery attempts';
console.error('getFormulus:', errorMsg);
reject(new Error(errorMsg));
} else {
setTimeout(checkForRecovery, 100);
}
};

// Start checking immediately
checkForRecovery();
}
});
};

// Also expose a synchronous check function for quick availability testing
window.formulusAvailable = function () {
const api = window.formulus || window.globalThis?.formulus;
return (
api && typeof api === 'object' && typeof api.getVersion === 'function'
);
};

console.log('getFormulus: Load script ready');
})();
21 changes: 20 additions & 1 deletion formulus/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require Pod::Executable.execute_command('node', ['-p',
{paths: [process.argv[1]]},
)', __dir__]).strip

platform :ios, min_ios_version_supported
platform :ios, '17.0'
prepare_react_native_project!

linkage = ENV['USE_FRAMEWORKS']
Expand Down Expand Up @@ -35,5 +35,24 @@ target 'Formulus' do
:mac_catalyst_enabled => false,
# :ccache_enabled => true
)

# Xcode 26+ rejects deployment targets below 15.0, but several pods
# (RNSVG, RNImagePicker, RNCAsyncStorage, ...) still declare 9.0-13.4.
# Clamp every pod target up to the RN minimum.
installer.pods_project.targets.each do |target|
target.build_configurations.each do |build_config|
current = build_config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']
if current && Gem::Version.new(current) < Gem::Version.new(min_ios_version_supported)
build_config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = min_ios_version_supported
end

# fmt 11.0.2 (pinned by RCT-Folly) fails C++20 consteval checks under
# Apple clang 21 (Xcode 26.4+); C++17 skips consteval entirely.
# Drop once RN ships fmt >= 12.1.0 (facebook/react-native#55601).
if target.name == 'fmt'
build_config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++17'
end
end
end
end
end
Loading
Loading