@zyno-io/mobile-foundation-rn
v26.824.2356
Published
Shared foundation library for React Native
Readme
@zyno-io/mobile-foundation-rn
Shared foundation library for React Native apps. Provides UI components, state management, services, and app lifecycle utilities to eliminate duplication across projects.
Installation
yarn add @zyno-io/mobile-foundation-rnPeer Dependencies
Your app must install these packages:
@expo/react-native-action-sheet@fortawesome/fontawesome-svg-core,@fortawesome/react-native-fontawesome@react-native-async-storage/async-storage@react-navigation/native,@react-navigation/stack@sentry/react-nativedetox(optional; required when using the Detox config plugin)expo(optional; required when using the Expo config plugin)expo-font,expo-linking,expo-splash-screen,expo-testflight,expo-updateslodashmobx,mobx-react-litereact,react-nativereact-native-device-info,react-native-gesture-handler,react-native-logs,react-native-reanimated,react-native-safe-area-context
Expo Config Plugins
The package includes reusable Expo config plugins for Android identity, raw
Gradle settings, and Detox native setup. They run during expo prebuild, so
generated Android files do not need to be committed or patched in CI.
Android Identity
Set an Android-only launcher name without changing expo.name. If the app's
installation ID differs from its Kotlin package, androidNamespace also keeps
the native source package stable while expo.android.package remains the
installation ID:
{
"expo": {
"android": {
"package": "app.zyno.talk_dev"
},
"plugins": [
[
"@zyno-io/mobile-foundation-rn",
{
"androidAppName": "ZynoTalk (Dev)",
"androidNamespace": "app.zyno.talk"
}
]
]
}
}During Expo Prebuild, the plugin writes
android/app/src/main/res/values/strings.xml, sets the Gradle namespace, and
keeps the Java/Kotlin source directories aligned with that namespace. Omit
androidNamespace when the namespace and expo.android.package are the same.
In a project that does not use Prebuild, set these native values manually.
Android Build Properties
expo-build-properties does not expose arbitrary gradle.properties values or
Gradle wrapper settings. The foundation plugin can set both:
{
"expo": {
"plugins": [
[
"@zyno-io/mobile-foundation-rn",
{
"androidBuildProperties": {
"gradleProperties": {
"org.gradle.jvmargs": "-Xmx2048m -XX:MaxMetaspaceSize=1024m"
},
"gradleWrapperProperties": {
"networkTimeout": 60000
}
}
}
]
]
}
}Both maps accept any property name and any string, number, or boolean value. Existing generated properties are replaced in place; missing properties are appended. For a project that only needs this capability, use the standalone entry point:
[
"@zyno-io/mobile-foundation-rn/plugin/android-build-properties",
{
"gradleProperties": {
"org.gradle.jvmargs": "-Xmx4g -XX:MaxMetaspaceSize=1g"
},
"gradleWrapperProperties": {
"networkTimeout": 60000
}
}
]Detox Android Setup
Install Detox in the consuming app, then opt into the foundation plugin:
yarn add --dev detox{
"expo": {
"android": {
"package": "app.example.dev"
},
"plugins": [
[
"@zyno-io/mobile-foundation-rn",
{
"androidNamespace": "app.example",
"detox": {
"masterTimeoutSec": 120,
"idleResourceTimeoutSec": 60,
"rnContextLoadTimeoutSec": 120
}
}
]
]
}
}The plugin adds the Detox Maven repository, Android test runner and
dependencies, duplicate-native-library packaging rules, manifest settings, and
DetoxTest.java. When androidNamespace is set on the combined foundation
plugin, the test class automatically uses that namespace instead of
expo.android.package. With the standalone entry point, pass androidPackage
when those values differ:
[
"@zyno-io/mobile-foundation-rn/plugin/detox",
{
"androidPackage": "app.example",
"allowCleartextTraffic": true,
"disableAutofill": true
}
]allowCleartextTraffic defaults to false; enable it only for a dedicated E2E
build that does not manage cleartext traffic through its own manifest or
network-security plugin. disableAutofill defaults to true for reliable E2E
runs. The plugin configures Android native code only; the consuming app still
owns its Detox configuration, Jest setup, tests, emulator/simulator selection,
and any project-specific network security or iOS synchronization workarounds.
Do not enable this plugin and @config-plugins/detox together. After migrating,
remove that package and the hand-maintained native Detox changes before running
a clean prebuild.
For the combined @zyno-io/mobile-foundation-rn entry point, set detox to
true for the defaults or to the options object shown above. The standalone
build-properties and Detox entry points can be used independently by any Expo
project without enabling the rest of the foundation config plugin.
Quick Start
1. Configure Foundation
Create a setup file (e.g. src/foundation/setup.ts) that runs before anything else:
import { faCheck, faSpinnerThird } from '@fortawesome/pro-regular-svg-icons';
import { Inter_400Regular, Inter_700Bold } from '@expo-google-fonts/inter';
import { configureFoundation, SentryHelper } from '@zyno-io/mobile-foundation-rn';
import { LightColors, DarkColors } from '@/constants/Colors';
import { Config } from '@/services/Config';
import { OpenApiUserError, OpenApiValidationError } from '@/services/ApiClient.setup';
configureFoundation({
colors: { light: LightColors, dark: DarkColors },
env: {
APP_ENV: Config.APP_ENV,
BUILD_VERSION: Config.BUILD_VERSION,
SENTRY_DSN: Config.SENTRY_DSN,
LOGGER_URL: Config.LOGGER_URL,
CDN_URL: Config.CDN_URL,
},
fonts: { Inter_400Regular, Inter_700Bold },
icons: { check: faCheck, spinner: faSpinnerThird },
statusBar: { barStyle: 'light-content', backgroundColorKey: 'background' },
splashScreen: 'auto', // default; use 'manual' to control splash yourself
updaterTimeout: 4000,
deepLinkHandler: (url) => handleDeepLink(url),
supportContact: '[email protected]',
userErrorClasses: [OpenApiUserError, OpenApiValidationError],
});
// Sentry is auto-initialized by configureFoundation2. Create App Storage
import { createAppStorage } from '@zyno-io/mobile-foundation-rn';
interface IAppStorage {
deviceToken?: string;
lastSyncTime?: number;
}
export const AppStorage = createAppStorage<IAppStorage>({});AppStorage is a MobX observable proxy with three special methods:
$load()— Loads persisted state from AsyncStorage (called automatically byuseSetupFoundation)$persist()— Manually triggers a persist$clear()— Clears all values and persists
Setting any property auto-persists with a 250ms debounce:
AppStorage.deviceToken = 'abc123'; // automatically saved3. Set Up Your App Entry
import 'react-native-reanimated';
import '@/foundation/setup';
import { MfProvider, useSetupFoundation } from '@zyno-io/mobile-foundation-rn';
import { AppNavigator } from './AppNavigator';
function RootLayout() {
const isReady = useSetupFoundation();
if (!isReady) return null;
return (
<MfProvider>
<AppNavigator />
</MfProvider>
);
}
export default RootLayout;Configuration Reference
configureFoundation(config)
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| colors | { light: ColorScheme; dark: ColorScheme } | Yes | Theme color definitions |
| env | { APP_ENV, BUILD_VERSION, SENTRY_DSN, LOGGER_URL, CDN_URL } | Yes | Environment config |
| icons | { check: IconProp; spinner: IconProp } | Yes | FontAwesome icons for checkbox and loader |
| fonts | Record<string, any> | No | Font map passed to useFonts() |
| statusBar | { barStyle, backgroundColorKey? } | No | Status bar styling |
| splashScreen | 'auto' \| 'manual' | No | Splash screen hide behavior (default: 'auto') |
| updaterTimeout | number | No | Max ms for each startup/checking phase; downloading uses 10 seconds and installation has no timeout |
| deepLinkHandler | (url: string) => void | No | Handler for queued launch URLs and subsequent deep-link events |
| supportContact | string | No | Contact info shown in error dialogs |
| userErrorClasses | ErrorClass[] | No | Error classes whose messages are shown to users |
| defaults | FoundationDefaults | No | Per-component style defaults (see below) |
Component defaults (config.defaults)
Optional overrides for the non-color style constants the components otherwise hardcode. Every field is optional — omit one and the library's built-in value is used, so existing apps are unaffected. *ColorKey fields name a key in your ColorScheme (resolved at render via the active theme):
defaults: {
fontFamily: 'Lato', // MfText (and anything that renders it); default 'Inter'
icon: { colorKey: 'white' }, // default color for a bare <MfIcon> with no `color`; default 'text'
button: {
backgroundColorKey: 'white', // default 'secondaryButtonBackground'
borderWidth: 1, // default 0
borderColorKey: 'cardBorder',
primaryBorderColorKey: 'primaryButtonBackground',
gap: 6, // icon↔text gap; default 0
titleFontSize: 16, // default 14
iconColorKey: 'white', // in-button icon color; default derives from title color
},
input: { borderRadius: 12, backgroundColorKey: 'inputBackground' }, // defaults 8 / 'cardBackground'
loader: { backgroundColor: 'transparent', overlayBackgroundColorKey: 'background' },
}Core Concepts
useSetupFoundation(appIsReady?)
Consolidates all foundation startup into a single hook:
- Initializes Sentry navigation instrumentation
- Runs the OTA updater hook
- Loads fonts via
useFonts() - Loads
AppMetaandAppStorage - Hides the splash screen when ready (unless
splashScreen: 'manual')
Pass an optional appIsReady callback for app-specific conditions:
const isReady = useSetupFoundation(() => myDataIsLoaded);<MfProvider>
Wraps your app with all required providers:
ActionSheetProviderThemeProvider(React Navigation)MfGlobalKeyboardProviderGestureHandlerRootViewSafeAreaProviderStatusBar(from config)DeepLinkingHandler(from config)GlobalLoaderOverlay(connected to foundation's internalLoaderState)
Accepts an optional colorScheme prop to force light/dark (defaults to system).
Observable Proxy (createObservableProxy)
Creates a MobX-observable proxy that automatically makes new properties observable:
const state = createObservableProxy({ count: 0 });
state.count = 1; // observable, triggers reactions
state.newProp = 'hello'; // also observableSupports hooks for custom getters and side effects:
const state = createObservableProxy(target, {
someProp: {
get: (target) => computedValue,
afterSet: () => persist(),
},
});Updater
Manages Expo OTA updates with observable status:
import { Updater } from '@zyno-io/mobile-foundation-rn';
import { observer } from 'mobx-react-lite';
const Startup = observer(() => {
// Updater.statusText is null when not busy
if (Updater.statusText) {
return <Text>{Updater.statusText}</Text>;
}
// proceed with app...
});Defer updates during critical operations:
Updater.setUpdateDeferralListener(() => isOnActiveCall);Components
| Component | Description |
|-----------|-------------|
| MfButton | Themed button with icon support and loading state |
| MfCheckbox | Checkbox using configured check icon |
| MfFlatList | Themed FlatList with pull-to-refresh |
| MfIcon | FontAwesome icon wrapper |
| MfLoader / MfLoaderView | Loading spinner using configured spinner icon |
| MfLoaderOverlay / GlobalLoaderOverlay | Full-screen loading overlay |
| MfScrollView | Themed ScrollView |
| MfText / MfStatusTextView | Themed text components |
| MfTextArea | Multi-line text input |
| MfTextInput | Single-line text input |
| MfWrapperView | Safe-area-aware wrapper view |
| MfForm / MfFormContext | Form context provider |
Hooks
| Hook | Description |
|------|-------------|
| useSetupFoundation() | Foundation startup (see above) |
| useWaitTask(logger, fn) | Returns a function that shows the loader overlay during execution |
| useStyles(styleGen) | Themed StyleSheet hook |
| useColors() | Current theme colors |
| useMountEffect(fn) | Effect that runs once on mount |
| useAppStateEffect(fn) | Runs on every app state change |
| useAppActivatedEffect(fn) | Runs when app comes to foreground |
| useAppDeactivatedEvent(fn) | Runs when app goes to background |
| useNavigationFocusEffect(fn) | Runs when screen gains focus |
| useNavigationUnfocusEffect(fn) | Runs when screen loses focus |
| useNavigationWithTitle(title) | Sets navigation title |
| useNavigationWithOptions(opts) | Sets navigation options |
| useNextTextInputRef() | Auto-focus next input on submit |
| useMfKeyboardHeight() | Current keyboard height |
| useMfSafeAreaInsets() | Safe area insets with overrides |
| getLinkingUrl() | Get the most recently observed linking URL synchronously |
Helpers
| Helper | Description |
|--------|-------------|
| createStyles(fn) | Create a themed style generator |
| createObservableProxy(target, hooks?) | MobX observable proxy |
| LoaderState | Internal observable { loaderCount } used by useWaitTask and GlobalLoaderOverlay |
| Broadcast / useBroadcastEffect | Event bus for cross-component communication |
| formatPhone, formatCurrency, formatDuration | Formatting utilities |
| memoizeAsync(fn) | Memoize an async function (runs once) |
| getCdnUrlForId(id) | CDN URL builder |
| hasHeightOrFlexProps(style) | Layout helper |
Services
| Service | Description |
|---------|-------------|
| AppMeta | Device/app metadata (deviceId, appVersion, appEnv, appEnvTf, isDevelopment) |
| createAppStorage(defaults) | Persistent observable storage factory |
| createLogger(name) | Structured logger with remote transport |
| UserError | User-facing error class |
| SentryHelper | Sentry initialization, wrapping, and navigation instrumentation |
| Updater | OTA update management with observable status |
Color Scheme
Define your colors using CreateColorScheme:
import { CreateColorScheme } from '@zyno-io/mobile-foundation-rn';
type AppColorKeys = 'brandPrimary' | 'brandSecondary';
export const LightColors: CreateColorScheme<AppColorKeys> = {
// base keys (required)
background: '#FFFFFF',
text: '#000000',
secondaryText: '#666666',
// ...other base keys
// custom keys
brandPrimary: '#007AFF',
brandSecondary: '#5856D6',
};File Structure
src/
config.ts — configureFoundation() and FoundationConfig type
types.ts — ColorScheme types
index.ts — Barrel exports
components/ — UI components
hooks/ — React hooks
helpers/ — Pure utilities (styles, observable, formatting, etc.)
services/ — Singletons (AppMeta, AppStorage, Logger, Sentry, Updater)
setup/ — Side-effect imports (Mobx, AppStateTracker)