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.
A non-clean expo prebuild merges into existing native folders and does not remove keys it no longer manages. After removing a plugin option or a permission, run pnpm prebuild:clean.

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 in plugins/, referenced by path from app.json. Both are idempotent and throw a clear error if the generated file is not what they expect.

withHealthConnectPermissionDelegate

Uses withMainActivity to edit the generated MainActivity.
  • Adds import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate after the package line.
  • Adds HealthConnectPermissionDelegate.setPermissionDelegate(this) straight after super.onCreate(...).
  • Throws if MainActivity is not Kotlin, has no package declaration, or has no super.onCreate(...) call.
react-native-health-connect launches its permission screen through this delegate. Without the registration the request cannot show.

withHealthConnectPermissionsRationale

Uses withAndroidManifest 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:
The Live Activity has buttons, for example marking a set done. With these the intent runs without opening the app and without asking for Face ID or a passcode on a locked phone.

@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_PATH out of the package folder into ~/Library/Caches/expo-modules-jsi/<hash>/DerivedData.
  • Passes CODE_SIGNING_ALLOWED=NO and CODE_SIGNING_REQUIRED=NO when building the framework slices.
This is a build-time fix only. The reason is not written down in the repository. It pairs with 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:
Before the patch an exception inside the coroutine was uncaught, which takes the app down. Now requestPermissions() rejects and the JS callers, all of which catch, carry on.

Other pnpm settings

pnpm-workspace.yaml also:
  • Overrides @types/react to 19.2.17.
  • Allows the unrs-resolver build script through allowBuilds.
.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:
  1. SVG as components. Sets babelTransformerPath to react-native-svg-transformer/expo, removes svg from assetExts and adds it to sourceExts. That is what lets iconRegistry.ts import .svg files as React components.
  2. Module shims. A resolveRequest hook swaps eight native-only packages for files in src/lib/web-shims when the platform is web. With EXPO_GO_SHIMS=1 it also swaps expo-widgets on 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.json plugins, permissions, entitlements or widgets.
  • Editing anything under plugins/, modules/ or patches/.
  • Changing the Expo SDK or React Native version.
JS that depends on such a change must also guard against running on older binaries. See OTA updates.