Files
orca/mobile/plugins/android-window-density.js
NeilandKelvin Amoaba 5d61e00c6d build(mobile): upgrade to Expo SDK 58 and React Native 0.88 (#37935)
* build(mobile): upgrade to Expo SDK 58 and React Native 0.88

Expo 58 drops Zod from @expo/cli, which clears the last Zod 3 advisory
(GHSA-mrhx-c7xc-m3w5) in mobile along with its local patch and override.

- Expo 55 -> 58, React Native 0.83 -> 0.88, React 19.3, gesture-handler 3,
  webview 14, reanimated 4.7, worklets 0.13 (SDK 58 bundled versions,
  pinned below today's batch to respect minimumReleaseAge).
- Port the react-native and react-native-webview patches unchanged; drop
  expo-constants, expo-notifications, @expo/cli, expo-router, zod and
  query-string patches plus the zod/query-string/xcode>uuid overrides,
  which upstream fixed or no longer need.
- iOS minimum rises to 16.4 (Expo 58 / RN 0.88 requirement).
- Migrate to RN 0.88 strict types: instance ref types, NamedStyles,
  AppState.currentState, list header/footer nulls.
- Replace removed runtime APIs: InteractionManager -> idle callback with a
  macrotask fallback; StyleSheet.absoluteFillObject -> absoluteFill;
  FileHandle.readBytes -> readBytesSync (it became async).
- Resolve react-native/asset-registry for shared assets in Metro and alias
  the old registry path for react-native-svg in the desktop mobile-web bundle.
- Reduce the iOS scene plugin to the reconnect override; Expo 58's template
  now ships the scene delegate itself.

* fix(mobile): bridge react-native-svg web imports and libsodium runtime for RN 0.88

- Share react-native-svg web shims between the mobile-web build and its render
  checks: map the old asset-registry path to react-native-web's registry and stub
  the Android-only touch-sound TurboModule.
- Patch react-native-libsodium 1.7.0 to read the JSI runtime from the bridge proxy;
  RN 0.88 removed RCTCxxBridge.runtime.

* fix(mobile): use Expo 58's legacy promise resolver for audio permissions

* fix(mobile): skip null handles in the RPC recording timer shims for RN 0.88 types

* style(mobile): format MobilePRSidebar after rebase

* fix(mobile): name RN 0.88 text input refs for declarations; allow queued back in stack render check

- Type text input refs with an exported interface so declaration emit can name hook results
  (RN 0.88 aliases TextInputInstance to an unexported class).
- Expo Router 58 drains back() after a commit; check the interrupted push's exit from the
  frame it starts, bounded to three frames of lag.

* chore(mobile): justify the text input handle interface for React Doctor

* chore(mobile): use a nameable intersection alias for text input refs

* fix(mobile): use sync expo-file-system writes, copies and moves on Expo 58

Expo 58 made File.write/copy/move and FileHandle.writeBytes return promises.
Our callers never awaited them, so failures escaped their try/catch and the
store's tmp-then-move and PDF writes could race their close or rename.

* build(mobile): raise the page bundle budget to 10 MiB for Expo 58

Expo 58 adds about 310 KB to the page: expo-router (+258 KB, now carrying
React Navigation, net about +110 KB) and Reanimated 4.7 (+152 KB). Main had
85 KB of headroom left. 10 MiB still stays within the 10 MiB per-asset contract.

* fix(mobile): drop the window-metrics seed RN 0.88 removed from Android

RN 0.88 no longer holds window metrics: DeviceInfoModule reads the activity's
WindowMetrics on demand and setWindowDisplayMetrics is gone, so MainActivity
no longer compiled. Layout scale still comes from the screen metrics we reseed.

---------

Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
2026-11-11 23:30:02 +00:00

103 lines
3.9 KiB
JavaScript

const { AndroidConfig, withAndroidManifest, withMainActivity } = require('expo/config-plugins')
// Why: One UI gives a split-screen pane its own density (#36305). Without `density` in
// configChanges Android recreates MainActivity, closing the open session and orphaning the image
// picker. Keeping the activity is not enough: React Native seeds its DisplayMetricsHolder from the
// application context, which keeps the full-screen density, so the pane is laid out at the wrong
// scale. MainActivity re-seeds it from its own resources after React Native does.
const MARKER = 'orca: window density'
const CLASS_HEADER = 'class MainActivity : ReactActivity() {'
const IMPORTS = [
'android.content.res.Configuration',
'android.os.Bundle',
'android.util.DisplayMetrics',
'com.facebook.react.uimanager.DisplayMetricsHolder'
]
const METHODS = `
// ${MARKER}, generated by plugins/android-window-density.js.
// React Native re-seeds from the application context when it creates the root view (onCreate)
// and in onConfigurationChanged, so these run before the next measure. Its root view's layout
// listener also re-seeds whenever the rotation differs from the last one it saw, after measuring;
// the pre-draw check runs after every layout listener and asks for another pass, in which React
// Native re-sends the window size to JS.
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
window.decorView.viewTreeObserver.addOnPreDrawListener {
val held = DisplayMetricsHolder.getScreenDisplayMetrics().densityDpi
if (held != resources.displayMetrics.densityDpi) {
useWindowDensity()
window.decorView.requestLayout()
}
true
}
}
override fun onResume() {
super.onResume()
useWindowDensity()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
useWindowDensity()
}
// React Native's initDisplayMetrics with the pane's density on the real display size, built before
// it is published because text measurement reads it off the UI thread.
@Suppress("DEPRECATION")
private fun useWindowDensity() {
val pane = resources.displayMetrics
val screen = DisplayMetrics().apply {
windowManager.defaultDisplay.getRealMetrics(this)
density = pane.density
densityDpi = pane.densityDpi
scaledDensity = pane.scaledDensity
}
DisplayMetricsHolder.setScreenDisplayMetrics(screen)
}
`
function patchMainActivity(contents) {
if (contents.includes(MARKER)) {
return contents
}
// A changed template must fail prebuild rather than drop or double an override.
if (
!contents.includes(CLASS_HEADER) ||
!contents.trimEnd().endsWith('}') ||
/fun (onPostCreate|onResume|onConfigurationChanged)\(/.test(contents)
) {
throw new Error(
"android-window-density: Expo's MainActivity template changed; update the plugin before building."
)
}
const imports = IMPORTS.filter((name) => !contents.includes(`import ${name}\n`))
.map((name) => `import ${name}\n`)
.join('')
const body = contents.trimEnd().slice(0, -1).trimEnd()
return `${body.replace(/^(package [^\n]+\n)/, `$1${imports}`)}\n${METHODS}}\n`
}
module.exports = function withAndroidWindowDensity(config) {
config = withAndroidManifest(config, (cfg) => {
const activity = AndroidConfig.Manifest.getMainActivityOrThrow(cfg.modResults)
const changes = (activity.$['android:configChanges'] ?? '').split('|').filter(Boolean)
if (!changes.includes('density')) {
activity.$['android:configChanges'] = [...changes, 'density'].join('|')
}
return cfg
})
return withMainActivity(config, (cfg) => {
if (cfg.modResults.language !== 'kt') {
throw new Error(
`android-window-density: expected a Kotlin MainActivity, got ${cfg.modResults.language}`
)
}
cfg.modResults.contents = patchMainActivity(cfg.modResults.contents)
return cfg
})
}