ios/ and android/ are generated by expo prebuild and ignored by Git. Native behaviour is defined in four tracked places:
Never hand-edit files under
ios/ or android/. The next prebuild will discard the change.
app.json
Identity and runtime
CFBundleDevelopmentRegion is he. Permission strings in app.json are written in Hebrew.
iOS entitlements and Info.plist
The widget extension target
ExpoWidgetsTarget is declared under extra.eas.build.experimental.ios.appExtensions with an app group entitlement, so EAS can sign it and the app can share files with it.
Android permissions
Requested:SCHEDULE_EXACT_ALARM, USE_BIOMETRIC, USE_FINGERPRINT, RECORD_AUDIO, CAMERA, WRITE_EXTERNAL_STORAGE, health.READ_STEPS, health.READ_HEALTH_DATA_IN_BACKGROUND.
Blocked with blockedPermissions, so libraries cannot add them back:
READ_EXTERNAL_STORAGE, READ_MEDIA_AUDIO, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_VISUAL_USER_SELECTED.
Plugins
expo-local-authentication is installed and configured, but no file under src/ imports it at present.
Local config plugins
Both are plain CommonJS files inplugins/, referenced by path from app.json. Both are idempotent and throw a clear error if the generated file is not what they expect.
withHealthConnectPermissionDelegate
UseswithMainActivity to edit the generated MainActivity.
- Adds
import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegateafter thepackageline. - Adds
HealthConnectPermissionDelegate.setPermissionDelegate(this)straight aftersuper.onCreate(...). - Throws if
MainActivityis not Kotlin, has no package declaration, or has nosuper.onCreate(...)call.
react-native-health-connect launches its permission screen through this delegate. Without the registration the request cannot show.
withHealthConnectPermissionsRationale
UseswithAndroidManifest to add an activity-alias named ViewPermissionUsageActivity:
This is the entry point Android uses to show an app’s reason for health permissions. The app treats it as required from API level 34: the automatic health prompt is gated on app version
1.0.6 there, the first build that carries the alias. See Health integration.
Local native module: cardio-notification
modules/cardio-notification is an Android-only Expo module. expo-module.config.json lists the android platform and the class expo.modules.cardionotification.CardioNotificationModule.
JS surface:
The wrapper is
src/features/movement/lib/cardioNotification.ts. It loads the module with requireOptionalNativeModule, so a binary built before the module existed gets null and the feature turns itself off.
The notification shows a native chronometer, so the elapsed time keeps counting without JS running. Behaviour is described in Live Activity and widgets and Cardio session.
Adding or changing a local module is a native change. It needs a new binary and cannot ship over the air.
Patches
pnpm-workspace.yaml lists four entries under patchedDependencies. pnpm applies them on install. Each patch is pinned to an exact version, so upgrading a patched package means re-creating its patch.
expo-widgets 57.0.2
File:ios/Widgets/AppIntent.swift. Adds two static properties to both WidgetUserInteraction and LiveActivityUserInteraction:
@expo/ui 57.0.3
Files:ios/ProgressView.swift and the matching TypeScript types. Adds a hidesCurrentValueLabel prop to the SwiftUI ProgressView. When true and a timerInterval is set, the view is built with an empty currentValueLabel.
SwiftUI draws a default countdown label under a timer-driven progress bar. The Live Activity rest bar shows its own time, so the default label is hidden.
expo-modules-jsi 57.0.0
File:apple/scripts/build-xcframework.sh. Two changes to the iOS build script:
- Moves
DERIVED_DATA_PATHout of the package folder into~/Library/Caches/expo-modules-jsi/<hash>/DerivedData. - Passes
CODE_SIGNING_ALLOWED=NOandCODE_SIGNING_REQUIRED=NOwhen building the framework slices.
usePrecompiledModules: false in expo-build-properties, which makes the build compile this framework from source.
react-native-health-connect 3.5.3
File:HealthConnectManager.kt. Wraps the permission dialog launch in try/catch and rejects the promise on failure:
requestPermissions() rejects and the JS callers, all of which catch, carry on.
Other pnpm settings
pnpm-workspace.yaml also:
- Overrides
@types/reactto19.2.17. - Allows the
unrs-resolverbuild script throughallowBuilds.
.npmrc sets legacy-peer-deps=true.
Metro and Babel
babel.config.js uses babel-preset-expo with no extra plugins. The React Compiler is turned on through experiments.reactCompiler in app.json.
metro.config.js does two things:
- SVG as components. Sets
babelTransformerPathtoreact-native-svg-transformer/expo, removessvgfromassetExtsand adds it tosourceExts. That is what letsiconRegistry.tsimport.svgfiles as React components. - Module shims. A
resolveRequesthook swaps eight native-only packages for files insrc/lib/web-shimswhen the platform isweb. WithEXPO_GO_SHIMS=1it also swapsexpo-widgetson native. See Web preview export and Guarding native imports.
What counts as a native change
Any of these needs a new store build and cannot be delivered by an OTA update:- Adding, removing or upgrading a package with native code.
- Editing
app.jsonplugins, permissions, entitlements or widgets. - Editing anything under
plugins/,modules/orpatches/. - Changing the Expo SDK or React Native version.