Skip to content

Comments

[WIP] Update to expo 54#5468

Open
janicduplessis wants to merge 37 commits intodevelopfrom
janic/expo-54
Open

[WIP] Update to expo 54#5468
janicduplessis wants to merge 37 commits intodevelopfrom
janic/expo-54

Conversation

@janicduplessis
Copy link
Contributor

@janicduplessis janicduplessis commented Jan 31, 2026

Summary

Upgrade to Expo 54, React Native 0.81, React 19, and Tamagui v2.0.0-rc.0 across the monorepo. This is a major dependency upgrade affecting both mobile and web platforms.

Changes

Core Upgrades

  • Expo 52 to 54: Updated Expo SDK, EAS config, and all Expo packages
  • React Native 0.73 to 0.81: Updated React Native and related dependencies
  • React 18 to 19: Updated React, React DOM, and adapted to new APIs
  • React Navigation v6 to v7: Updated navigation libraries
  • Tamagui 1.x to 2.0.0-rc.0: Updated Tamagui UI framework
  • Node.js 18 to 22: Updated Node.js version requirement

Mobile Platform Changes

  • Migrated iOS AppDelegate from Objective-C to Swift
  • Migrated Android MainActivity/MainApplication from Java to Kotlin
  • Renamed index.js to index.tsx
  • Moved Cosmos config to mobile app directory
  • Updated build configuration and bundle generation for Expo 54
  • Migrated from react-native-clipboard to expo-clipboard
  • Updated PostHog integration for Expo 54

Web Platform Fixes

  • Fixed production build: added Vite preview proxy and expo polyfill
  • Fixed envPrefix override by Tamagui vite-plugin (was hiding VITE_ env vars)
  • Fixed Tamagui v2 dialog rendering issues:
    • Patched @tamagui/dialog to remove render: 'dialog' (native dialog element causes stacking context issues)
    • Added disableRemoveScroll to ActionSheet dialog mode
    • Fixed 0-height dialog content caused by ScrollView flex={1} changed to flexShrink={1}
    • Fixed text trimming margins causing click target overlap in GroupTypeCard

Other

  • Fixed React 19 and React Native compatibility issues across shared packages
  • Added customizable hoverStyle prop to ChannelListItem and GroupListItem
  • Updated rube Dockerfile Node.js setup to v22
  • Fixed setTimeout type conflict in e2e test runner

How did I test?

  • Ran production smoke e2e test locally (passes)
  • Ran create-group e2e test locally (dialog interactions work, groups create successfully)
  • Verified VITE_ environment variables load correctly in dev and preview modes
  • Verified dialog overlays no longer block pointer events on web

Risks and impact

  • Safe to rollback without consulting PR author? Yes
  • Affects important code area:
    • Onboarding
    • State / providers
    • Message sync
    • Channel display
    • Notifications
    • Other: Build system, all UI components (Tamagui upgrade), mobile native code

Rollback plan

Revert the entire branch. Due to the scope of the upgrade (Expo, React Native, React, Tamagui), partial rollbacks are not feasible.

Screenshots / videos

N/A - infrastructure/dependency upgrade

import * as store from '@tloncorp/shared/store';
import * as utils from '@tloncorp/shared/utils';
import * as LibPhone from 'libphonenumber-js';
import { parsePhoneNumberFromString } from 'libphonenumber-js';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metro now supports es modules, but it seems like its implementation is incompatible with something libphonenumber-js does. When imported via * as it causes an error.

// handle paste button click
const onHandlePasteClick = useCallback(async () => {
const clipboardContents = await Clipboard.getString();
const clipboardContents = await Clipboard.getStringAsync();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@react-native-clipboard/clipboard was causing react-native-windows and react-native-mac to be imported, which in turn imported older version of react-native causing some conflicts, ended up migrating to expo-clipboard, which we were already also using.

Was there a specific reason for using it?

import { Component } from 'react';
import { NativeFixtureLoader } from 'react-cosmos-native';

import { moduleWrappers, rendererConfig } from '../../../cosmos.imports';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the cleanup to how we handle monorepos importing from the repo root no longer works. I moved the cosmos.imports to the tlon-mobile folder instead.

const routeNameRef = useRef<string>(undefined);
const migrationState = useMigrations();
const splashIsHidden = useSplashHider();
const navigationLogging = useNavigationLogging();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is required because of the react-navigation 7 update, the recommended way to subscribe to navigation state changes is here with NavigationContainer props. This refactors posthog screen tracking to their recommended approach for react-navigation 7, as well as our useNavigationLogging hook.

api.cache(true);
return {
presets: [['babel-preset-expo', { jsxRuntime: 'automatic' }]],
presets: [['babel-preset-expo', { unstable_transformImportMeta: true }]],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

required for zustand to work

</TailwindProvider>
);
}

function Main(props) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed this file to TS, no props were ever passed here.

const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '../..');
const config = getSentryExpoConfig(projectRoot);
const baseConfig = getSentryExpoConfig(projectRoot);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expo after sdk 52 supports monorepos without any extra config, so just removed all configs related to it, and it still works!

getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there any reason for disabling inline requires? Usually helps with performance.

"ios:preview": "expo run:ios --scheme=Landscape-preview",
"generate": "pnpm generate:tailwind && pnpm run generate:ios",
"generate:ios": "react-native bundle --entry-file='apps/tlon-mobile/index.js' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios' --assets-dest='./ios'",
"generate:ios": "expo export:embed --platform ios --bundle-output ./ios/main.jsbundle --assets-dest ./ios",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should always use expo-cli to build bundle

"lodash": "^4.17.21",
"posthog-react-native": "^2.7.1",
"react": "^18.3.1",
"posthog-react-native": "^4.27.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to bump posthog for compat with react-navigation 7

"react-native-worklets": "^0.7.2",
"seedrandom": "^3.0.5",
"tailwind-rn": "^4.2.0",
"text-encoding": "^0.7.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seemed unused

"@react-native-firebase/app": "^22.0.0",
"@react-native-firebase/crashlytics": "^22.0.0",
"@react-native-firebase/perf": "^22.0.0",
"@react-navigation/bottom-tabs": "^6.5.12",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to update to react-navigation v7, since the drawer component in react-navigation v6 doesn't work with reanimated v4

"react-native-country-codes-picker@2.3.5": "patches/react-native-country-codes-picker@2.3.5.patch",
"expo@52.0.47": "patches/expo@52.0.47.patch",
"expo-background-task@0.1.4": "patches/expo-background-task@0.1.4.patch",
"expo-localization@16.0.1": "patches/expo-localization@16.0.1.patch"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"@urbit/http-api@3.1.0-dev-3": "patches/@urbit__http-api@3.1.0-dev-3.patch",
"tailwind-rn@4.2.0": "patches/tailwind-rn@4.2.0.patch",
"usehooks-ts@2.6.0": "patches/usehooks-ts@2.6.0.patch",
"react-native-reanimated@3.8.1": "patches/react-native-reanimated@3.8.1.patch",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% sure about that one, seems to be for fix some issues on web, I assume it would error if still needed.

@janicduplessis janicduplessis force-pushed the janic/expo-54 branch 5 times, most recently from de77392 to cbab19c Compare February 4, 2026 04:35
Major version upgrades:
- Expo 52 → 54
- React Native 0.73 → 0.81
- React 18 → 19
- React Navigation 6 → 7
- PostHog and other dependencies updated for compatibility
- Delete AppDelegate.h and AppDelegate.mm
- Add AppDelegate.swift with equivalent functionality
- Remove main.m (Swift uses @main attribute)
- Update Xcode project configuration
- Convert MainActivity.java to MainActivity.kt
- Convert MainApplication.java to MainApplication.kt
- Preserve all functionality including window insets handling
Mobile:
- Update app.config.ts for Expo 54 plugins
- Update babel.config.js for new architecture
- Update metro.config.js with new resolver config
- Update Android gradle and build configuration
- Update iOS Info.plist and bridging headers

Web:
- Update Vite config for React Native Worklets
Expo 54 supports TypeScript entry files
- Move cosmos.imports.ts from root to apps/tlon-mobile/
- Add cosmos.config.json to tlon-mobile app
- Remove root cosmos.imports.ts
- Refactor navigation logging to work with React Navigation v7
- Remove unnecessary React imports (React 19 auto-imports)
- Update TypeScript config for React 19
- Fix navigation context usage
- Refactor PostHog initialization to synchronous pattern
- Update telemetry provider for new PostHog API
- Fix type compatibility with new PostHog version
- Update pnpm-lock.yaml with new dependency versions
- Update iOS Podfile.lock
- Update Android gradle.lockfile
- Rename patch from @tamagui__sheet@1.126.12 to @tamagui__sheet@1.126.18
- Update pnpm.patchedDependencies reference
- Update lockfiles with new dependency resolutions
- Minor dependency version bumps from pnpm install
- Replace 'react-native bundle' with 'expo export:embed'
- Remove outdated entry-file path and dev flag
- Expo CLI automatically handles entry point detection
- Replace all imports with expo-clipboard
- Update Clipboard.setString() to Clipboard.setStringAsync()
- Update Clipboard.getString() to Clipboard.getStringAsync()
- Update Clipboard.hasImage() to Clipboard.hasImageAsync()
- Update Clipboard.getImagePNG/JPG() to Clipboard.getImageAsync({ format })
- Update test mocks to use expo-clipboard mock
- Make callbacks async where needed for await usage
- Revert @tamagui/sheet patch from 1.126.18 to 1.126.12
- Lock all Tamagui dependencies to exact 1.126.12 (no range)
- Update all package.json files to use strict version (removed ~)
- Regenerate lockfiles with clean install
- All Tamagui packages now on exact 1.126.12
- Change Tamagui versions from exact 1.126.12 to ~1.126.12 range
  - @tamagui/react-native-media-driver
  - @tamagui/babel-plugin
  - @tamagui/vite-plugin
- Add @react-native-picker/picker@^2.11.4 as explicit dependency
  - Was peer dependency of react-native-phone-input
  - Now explicitly managed for Expo 54 compatibility
- Update Android build scripts: productionDebugOptimized → productionDebug
- Regenerate lockfiles
- TelemetryProvider: Remove null check, use disabled option for tests
- AppInfoScreen: Format upload logs button
- tsconfig: Remove expo/tsconfig.base extend, add JSDoc comment
Changes applied:
- Android: Set DEFAULT_INTERVAL_MINUTES to 20 minutes (was 24 hours)
- iOS: Set intervalSeconds to 15 minutes (was 12 hours)
- iOS: Change from BGProcessingTaskRequest to BGAppRefreshTaskRequest
- iOS: Change background mode check from 'processing' to 'fetch'
- iOS: Set earliestBeginDate to nil for immediate scheduling
- iOS: Add debug print statements for task lifecycle
- Remove network/power requirements from iOS task requests

Removed obsolete patches:
- expo-background-task@0.1.4.patch
- @react-navigation__drawer@6.7.2.patch
- expo-localization@16.0.1.patch
- react-native-reanimated@3.8.1.patch
- react-native@0.73.4.patch

These were automatically removed as those versions are no longer installed.
- Upgrade @tamagui/* packages from ~1.126.12 to ~2.0.0-rc.0
- Remove @tamagui/sheet patch (no longer needed in v2)

Tamagui v2 API changes:
- Replace Stack with View/YStack (Stack removed in v2)
- Replace animation prop with transition on animated components
- Replace tag prop with render (renamed in v2)
- Replace editable prop with readOnly on TextArea/Input
- Replace onHoverIn/onHoverOut with onMouseEnter/onMouseLeave
- Remove textWrap/wordWrap non-existent props
- Remove fontWeight from Button (stack-based, not text)
- Fix TransitionProp by adding medium/slow animation keys to config

React 19 type compatibility:
- JSX.Element → ReactNode/ReactElement for children props
- RefObject<T> → RefObject<T | null> for ref nullability
- Fix generic types for forwardRef components

React Native API updates:
- BackHandler.addEventListener returns NativeEventSubscription
- headerBackTitleVisible → headerBackButtonDisplayMode: 'minimal'
- expo-contacts types: Contact → ExistingContact

Component fixes:
- Update ButtonContext/FloatingActionButton to use useStyledContext()
- Fix Pressable navigation props (href/action typing)
- Fix OverflowTriggerButton forwardRef generic type
- Cast web-only outlineStyle in BareChatInput
- Provide defaultTheme fallback in BaseProviderStack
- Remove duplicate clipboard image format attempt
- Update react-native-country-codes-picker patch for JSX.Element
- Add fontFamily to ListItemTitle for consistency
- Add position="relative" for absolute positioning contexts
Allow overriding the default hover background color by accepting
a hoverStyle prop, defaulting to the original behavior if not provided.
- Fix useRef initialization to explicitly pass undefined
- Update BackHandler event listener cleanup to use new API
- Fix ts-expect-error placement in PhoneNumberInput
- Add type cast for RawBottomSheetTextInput ref
Replace NodeJS.Timeout type with ReturnType<typeof setTimeout> to resolve
compilation error when DOM types are present in tsconfig. This makes the
type declaration work correctly in both DOM and Node.js environments.
- Add preview.proxy to vite config so vite preview proxies API requests
  to the Urbit ship (the urbit plugin only sets server.proxy for dev)
- Add resolveId hook to reactNativeWebPlugin to prefer .web.ts index
  files for directory imports in node_modules (fixes Rollup resolving
  expo-modules-core polyfill/index.ts noop instead of index.web.ts)
- Add explicit expo-polyfill.ts imported first in main.tsx to ensure
  globalThis.expo is set up before any expo modules load
- Add envPrefix ['VITE_', 'TAMAGUI_'] to vite config to prevent
  Tamagui plugin from overriding Vite's default VITE_ prefix
- Fix ActionSheet dialog: add disableRemoveScroll to prevent z-index
  stacking issues, change ScrollView flex to flexShrink to fix
  0-height content rendering
- Patch @tamagui/dialog to remove render: 'dialog' on DialogPortalFrame
  which causes stacking context issues with native <dialog> element
- Fix GroupTypeCard text overlap by disabling text trimming margins
The previous fix only addressed ActionSheet, but ConfirmDialog (used for
delete group confirmation) had the same issue. Add pointerEvents="none"
to Dialog.Overlay in both ActionSheet and ConfirmDialog so overlays never
intercept clicks, regardless of whether the pnpm patch is applied.

Also make e2e test cleanup more robust by pressing Escape to dismiss any
lingering dialogs before attempting to interact with background elements.
Fixes any-ascii ESM crash during Tamagui static extraction on EAS.
Node 22.12.0 had buggy require(esm) interop that caused uncatchable
errors with esbuild-register.
React Nav v7 changed navigate() to push new screens instead of popping
back to existing ones in a stack. This caused duplicate ChannelRoot
screens (and 2 MessageInput textareas) when navigating back to a channel
from GroupSettings via the sidebar.

- Add pop: true to getDesktopChannelRoute nested params so navigate()
  pops back to existing ChannelRoot instead of pushing a new one
- Add pop: true to useNavigateBackFromPost desktop path
- Fix navigateToGroupSettings to navigate directly to Channel >
  GroupSettings in a single call instead of the broken 2-step approach
  (navigateToGroup + setTimeout with stale navigation ref)
…ade issues

React Navigation v7 changed how nested navigator state is handled:
in v6, navigating with `params: { screen, params }` would reset the
nested navigator state. In v7, it dispatches `CommonActions.navigate()`
which pushes onto the existing stack, causing stale screens to
accumulate.

The fix uses `params: { state: { routes: [...], index: 0 } }` which
triggers `CommonActions.reset()` to fully replace the nested state,
matching v6 behavior. This is applied to all GroupSettings stack
navigations.

Also adds `pop: true` to navigate calls that should pop back to
existing screens (restoring v6 popTo behavior), and fixes several
other issues from the Expo 54 / RN v7 upgrade:

- Port @tamagui/sheet patch to v2.0.0-rc.0
- Move ForwardPostSheetProvider inside NavigationContainer
- Fix ActionSheet Popover z-index behind modals
- Add navigation state debug logging
- Refactor e2e tests to use navigateBack helper
NativeStack on web renders all stacked screens in the DOM, creating
many HeaderBackButton elements. The old helper only checked indices
[2, 1, 0] which missed the correct button when 5+ screens were stacked.

Now dynamically counts all back buttons and clicks the last visible one
(the topmost screen). Also fix roles-management test back-navigation
loops to stop once they reach the group channels view instead of
blindly navigating back a fixed number of times.
@janicduplessis janicduplessis marked this pull request as ready for review February 20, 2026 20:07
@janicduplessis janicduplessis changed the title Update to expo 54 [WIP] Update to expo 54 Feb 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant