From 18f3464fab367f6cf13d484b6d4de03256ed6def Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Tue, 5 May 2026 09:38:14 +0200 Subject: [PATCH 1/3] [docs] notifications do not require running on real devices (#45363) # Why notifications do not require real devices # How update docs to not require real device for push notifications # Test Plan # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- .../push-notifications-setup.mdx | 69 +++++++++---------- .../unversioned/sdk/notifications.mdx | 63 ++++++++--------- .../versions/v54.0.0/sdk/notifications.mdx | 63 ++++++++--------- .../versions/v55.0.0/sdk/notifications.mdx | 63 ++++++++--------- 4 files changed, 118 insertions(+), 140 deletions(-) diff --git a/docs/pages/push-notifications/push-notifications-setup.mdx b/docs/pages/push-notifications/push-notifications-setup.mdx index 9b0574f06774fc..87b0aceb1a27cc 100644 --- a/docs/pages/push-notifications/push-notifications-setup.mdx +++ b/docs/pages/push-notifications/push-notifications-setup.mdx @@ -36,9 +36,9 @@ If you need finer-grained control over your notifications, communicating directl - - Push notifications are not supported on Android Emulators or iOS Simulators. You will need a - real device to test. + + You can test push notifications on a physical Android or iOS device, on an Android Emulator with + Google Play services, or on an iOS Simulator running on Xcode 14 or later (macOS 13+, iOS 16+). @@ -48,12 +48,11 @@ The following steps in this guide use [EAS Build](/build/introduction/). This is ## Install libraries -Run the following command to install the `expo-notifications`, `expo-device` and `expo-constants` libraries: +Run the following command to install the `expo-notifications` and `expo-constants` libraries: - + - [`expo-notifications`](/versions/latest/sdk/notifications) library is used to request a user's permission and to obtain the `ExpoPushToken` for sending push notifications. -- [`expo-device`](/versions/latest/sdk/device) is used to check whether the app is running on a physical device. - [`expo-constants`](/versions/latest/sdk/constants) is used to get the `projectId` value from the app config. @@ -88,7 +87,6 @@ The code below shows a working example of how to register for, send, and receive ```tsx App.tsx import { useState, useEffect } from 'react'; import { Text, View, Button, Platform } from 'react-native'; -import * as Device from 'expo-device'; import * as Notifications from 'expo-notifications'; import Constants from 'expo-constants'; @@ -140,37 +138,32 @@ async function registerForPushNotificationsAsync() { }); } - if (Device.isDevice) { - const { status: existingStatus } = await Notifications.getPermissionsAsync(); - let finalStatus = existingStatus; - if (existingStatus !== 'granted') { - const { status } = await Notifications.requestPermissionsAsync(); - finalStatus = status; - } - if (finalStatus !== 'granted') { - handleRegistrationError('Permission not granted to get push token for push notification!'); - return; - } - const projectId = - Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId; - if (!projectId) { - handleRegistrationError('Project ID not found'); - } - try { - /* @info This fetches the Expo push token (if not previously fetched), which is unique to this device and projectID. */ - const pushTokenString = ( - await Notifications.getExpoPushTokenAsync({ - projectId, - }) - ).data; - /* @end */ - console.log(pushTokenString); - return pushTokenString; - } catch (e: unknown) { - handleRegistrationError(`${e}`); - } - } else { - handleRegistrationError('Must use physical device for push notifications'); + const { status: existingStatus } = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + if (finalStatus !== 'granted') { + handleRegistrationError('Permission not granted to get push token for push notification!'); + return; + } + const projectId = Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId; + if (!projectId) { + handleRegistrationError('Project ID not found'); + } + try { + /* @info This fetches the Expo push token (if not previously fetched), which is unique to this device and projectID. */ + const pushTokenString = ( + await Notifications.getExpoPushTokenAsync({ + projectId, + }) + ).data; + /* @end */ + console.log(pushTokenString); + return pushTokenString; + } catch (e: unknown) { + handleRegistrationError(`${e}`); } } diff --git a/docs/pages/versions/unversioned/sdk/notifications.mdx b/docs/pages/versions/unversioned/sdk/notifications.mdx index bb8c959fddc508..c400138e0224cc 100644 --- a/docs/pages/versions/unversioned/sdk/notifications.mdx +++ b/docs/pages/versions/unversioned/sdk/notifications.mdx @@ -4,7 +4,7 @@ description: A library that provides an API to fetch push notification tokens an sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-notifications' packageName: 'expo-notifications' iconUrl: '/static/images/packages/expo-notifications.png' -platforms: ['android*', 'ios*'] +platforms: ['android', 'ios'] --- import { NotificationBoxIcon } from '@expo/styleguide-icons/outline/NotificationBoxIcon'; @@ -65,14 +65,13 @@ This issue only affects debug builds and does not occur in release builds. To wo ## Usage -Check out the example Snack below to see Notifications in action, make sure to use a physical device to test it. Push notifications don't work on emulators/simulators. +Check out the example Snack below to see Notifications in action. Push notifications work on physical devices, Android emulators with Google Play services, and iOS simulators on Xcode 14 or later (macOS 13+, iOS 16+). - + ```tsx import { useState, useEffect } from 'react'; import { Text, View, Button, Platform } from 'react-native'; -import * as Device from 'expo-device'; import * as Notifications from 'expo-notifications'; import Constants from 'expo-constants'; @@ -166,37 +165,33 @@ async function registerForPushNotificationsAsync() { }); } - if (Device.isDevice) { - const { status: existingStatus } = await Notifications.getPermissionsAsync(); - let finalStatus = existingStatus; - if (existingStatus !== 'granted') { - const { status } = await Notifications.requestPermissionsAsync(); - finalStatus = status; - } - if (finalStatus !== 'granted') { - alert('Failed to get push token for push notification!'); - return; - } - // Learn more about projectId: - // https://docs.expo.dev/push-notifications/push-notifications-setup/#configure-projectid - // EAS projectId is used here. - try { - const projectId = - Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId; - if (!projectId) { - throw new Error('Project ID not found'); - } - token = ( - await Notifications.getExpoPushTokenAsync({ - projectId, - }) - ).data; - console.log(token); - } catch (e) { - token = `${e}`; + const { status: existingStatus } = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + if (finalStatus !== 'granted') { + alert('Failed to get push token for push notification!'); + return; + } + // Learn more about projectId: + // https://docs.expo.dev/push-notifications/push-notifications-setup/#configure-projectid + // EAS projectId is used here. + try { + const projectId = + Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId; + if (!projectId) { + throw new Error('Project ID not found'); } - } else { - alert('Must use physical device for Push Notifications'); + token = ( + await Notifications.getExpoPushTokenAsync({ + projectId, + }) + ).data; + console.log(token); + } catch (e) { + token = `${e}`; } return token; diff --git a/docs/pages/versions/v54.0.0/sdk/notifications.mdx b/docs/pages/versions/v54.0.0/sdk/notifications.mdx index 2c24d3974234dd..576282db0c806e 100644 --- a/docs/pages/versions/v54.0.0/sdk/notifications.mdx +++ b/docs/pages/versions/v54.0.0/sdk/notifications.mdx @@ -4,7 +4,7 @@ description: A library that provides an API to fetch push notification tokens an sourceCodeUrl: 'https://github.com/expo/expo/tree/sdk-54/packages/expo-notifications' packageName: 'expo-notifications' iconUrl: '/static/images/packages/expo-notifications.png' -platforms: ['android*', 'ios*'] +platforms: ['android', 'ios'] --- import { NotificationBoxIcon } from '@expo/styleguide-icons/outline/NotificationBoxIcon'; @@ -65,14 +65,13 @@ This issue only affects debug builds and does not occur in release builds. To wo ## Usage -Check out the example Snack below to see Notifications in action, make sure to use a physical device to test it. Push notifications don't work on emulators/simulators. +Check out the example Snack below to see Notifications in action. Push notifications work on physical devices, Android emulators with Google Play services, and iOS simulators on Xcode 14 or later (macOS 13+, iOS 16+). - + ```tsx import { useState, useEffect } from 'react'; import { Text, View, Button, Platform } from 'react-native'; -import * as Device from 'expo-device'; import * as Notifications from 'expo-notifications'; import Constants from 'expo-constants'; @@ -166,37 +165,33 @@ async function registerForPushNotificationsAsync() { }); } - if (Device.isDevice) { - const { status: existingStatus } = await Notifications.getPermissionsAsync(); - let finalStatus = existingStatus; - if (existingStatus !== 'granted') { - const { status } = await Notifications.requestPermissionsAsync(); - finalStatus = status; - } - if (finalStatus !== 'granted') { - alert('Failed to get push token for push notification!'); - return; - } - // Learn more about projectId: - // https://docs.expo.dev/push-notifications/push-notifications-setup/#configure-projectid - // EAS projectId is used here. - try { - const projectId = - Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId; - if (!projectId) { - throw new Error('Project ID not found'); - } - token = ( - await Notifications.getExpoPushTokenAsync({ - projectId, - }) - ).data; - console.log(token); - } catch (e) { - token = `${e}`; + const { status: existingStatus } = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + if (finalStatus !== 'granted') { + alert('Failed to get push token for push notification!'); + return; + } + // Learn more about projectId: + // https://docs.expo.dev/push-notifications/push-notifications-setup/#configure-projectid + // EAS projectId is used here. + try { + const projectId = + Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId; + if (!projectId) { + throw new Error('Project ID not found'); } - } else { - alert('Must use physical device for Push Notifications'); + token = ( + await Notifications.getExpoPushTokenAsync({ + projectId, + }) + ).data; + console.log(token); + } catch (e) { + token = `${e}`; } return token; diff --git a/docs/pages/versions/v55.0.0/sdk/notifications.mdx b/docs/pages/versions/v55.0.0/sdk/notifications.mdx index bb8c959fddc508..c400138e0224cc 100644 --- a/docs/pages/versions/v55.0.0/sdk/notifications.mdx +++ b/docs/pages/versions/v55.0.0/sdk/notifications.mdx @@ -4,7 +4,7 @@ description: A library that provides an API to fetch push notification tokens an sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-notifications' packageName: 'expo-notifications' iconUrl: '/static/images/packages/expo-notifications.png' -platforms: ['android*', 'ios*'] +platforms: ['android', 'ios'] --- import { NotificationBoxIcon } from '@expo/styleguide-icons/outline/NotificationBoxIcon'; @@ -65,14 +65,13 @@ This issue only affects debug builds and does not occur in release builds. To wo ## Usage -Check out the example Snack below to see Notifications in action, make sure to use a physical device to test it. Push notifications don't work on emulators/simulators. +Check out the example Snack below to see Notifications in action. Push notifications work on physical devices, Android emulators with Google Play services, and iOS simulators on Xcode 14 or later (macOS 13+, iOS 16+). - + ```tsx import { useState, useEffect } from 'react'; import { Text, View, Button, Platform } from 'react-native'; -import * as Device from 'expo-device'; import * as Notifications from 'expo-notifications'; import Constants from 'expo-constants'; @@ -166,37 +165,33 @@ async function registerForPushNotificationsAsync() { }); } - if (Device.isDevice) { - const { status: existingStatus } = await Notifications.getPermissionsAsync(); - let finalStatus = existingStatus; - if (existingStatus !== 'granted') { - const { status } = await Notifications.requestPermissionsAsync(); - finalStatus = status; - } - if (finalStatus !== 'granted') { - alert('Failed to get push token for push notification!'); - return; - } - // Learn more about projectId: - // https://docs.expo.dev/push-notifications/push-notifications-setup/#configure-projectid - // EAS projectId is used here. - try { - const projectId = - Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId; - if (!projectId) { - throw new Error('Project ID not found'); - } - token = ( - await Notifications.getExpoPushTokenAsync({ - projectId, - }) - ).data; - console.log(token); - } catch (e) { - token = `${e}`; + const { status: existingStatus } = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + if (finalStatus !== 'granted') { + alert('Failed to get push token for push notification!'); + return; + } + // Learn more about projectId: + // https://docs.expo.dev/push-notifications/push-notifications-setup/#configure-projectid + // EAS projectId is used here. + try { + const projectId = + Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId; + if (!projectId) { + throw new Error('Project ID not found'); } - } else { - alert('Must use physical device for Push Notifications'); + token = ( + await Notifications.getExpoPushTokenAsync({ + projectId, + }) + ).data; + console.log(token); + } catch (e) { + token = `${e}`; } return token; From 29c0239a92a4b4a78fdbd1cb272e382c482b7884 Mon Sep 17 00:00:00 2001 From: Jakub Tkacz <32908614+Ubax@users.noreply.github.com> Date: Tue, 5 May 2026 09:46:06 +0200 Subject: [PATCH 2/3] [doctor] add check for both expo-router and react-navigation installed in the same project (#45323) # Why As of SDK 56, having both router and react-navigation installed in the same project, suggests an issue or need to perform the router migration. Add the expo-doctor check for better visibility # How 1. Detect if both are installed 2. Show error and migration guide link for next steps # Test Plan 1. CI 2. Router project with `@react-navigation` installed directly as dependency Screenshot 2026-05-04 at 10 00 53 # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- packages/expo-doctor/CHANGELOG.md | 1 + .../checks/ExpoRouterReactNavigationCheck.ts | 40 +++++ .../ExpoRouterReactNavigationCheck.test.ts | 161 ++++++++++++++++++ .../expo-doctor/src/utils/checkResolver.ts | 2 + 4 files changed, 204 insertions(+) create mode 100644 packages/expo-doctor/src/checks/ExpoRouterReactNavigationCheck.ts create mode 100644 packages/expo-doctor/src/checks/__tests__/ExpoRouterReactNavigationCheck.test.ts diff --git a/packages/expo-doctor/CHANGELOG.md b/packages/expo-doctor/CHANGELOG.md index 24e792ae157e78..ff73204f961da8 100644 --- a/packages/expo-doctor/CHANGELOG.md +++ b/packages/expo-doctor/CHANGELOG.md @@ -9,6 +9,7 @@ - Add version to the `--verbose` output ([#44592](https://github.com/expo/expo/pull/44592) by [@kitten](https://github.com/kitten)) - Add check that warns about invalid `overrides`/`resolutions` for critical package versions ([#44770](https://github.com/expo/expo/pull/44770) by [@kitten](https://github.com/kitten)) - add a warning when mixing `@expo/vector-icons` and `react-native-vector-icons` or packages from `@react-native-vector-icons` ([#37958](https://github.com/expo/expo/pull/37958) by [@vonovak](https://github.com/vonovak)) +- Add check for both expo-router and react-navigation installed in same project ([#45323](https://github.com/expo/expo/pull/45323) by [@Ubax](https://github.com/Ubax)) ### 🐛 Bug fixes diff --git a/packages/expo-doctor/src/checks/ExpoRouterReactNavigationCheck.ts b/packages/expo-doctor/src/checks/ExpoRouterReactNavigationCheck.ts new file mode 100644 index 00000000000000..76da1963f2b08f --- /dev/null +++ b/packages/expo-doctor/src/checks/ExpoRouterReactNavigationCheck.ts @@ -0,0 +1,40 @@ +import type { DoctorCheck, DoctorCheckParams, DoctorCheckResult } from './checks.types'; + +export class ExpoRouterReactNavigationCheck implements DoctorCheck { + description = 'Check that @react-navigation packages are not installed alongside expo-router'; + + sdkVersionRange = '>=56.0.0 <57.0.0'; + + async runAsync({ pkg }: DoctorCheckParams): Promise { + const hasExpoRouter = !!( + pkg.dependencies?.['expo-router'] || pkg.devDependencies?.['expo-router'] + ); + if (!hasExpoRouter) { + return { isSuccessful: true, issues: [], advice: [] }; + } + + const reactNavigationDeps = [ + ...Object.keys(pkg.dependencies ?? {}), + ...Object.keys(pkg.devDependencies ?? {}), + ] + .filter((name) => name.startsWith('@react-navigation/')) + .filter((name, index, all) => all.indexOf(name) === index) + .sort(); + + if (reactNavigationDeps.length === 0) { + return { isSuccessful: true, issues: [], advice: [] }; + } + + const list = reactNavigationDeps.map((n) => `"${n}"`).join(', '); + + return { + isSuccessful: false, + issues: [ + `As of SDK 56, expo-router is no longer compatible with react-navigation. The following @react-navigation packages are installed as direct dependencies and should be removed: ${list}.`, + ], + advice: [ + `Remove these packages from your package.json and replace any direct \`@react-navigation/*\` imports in your code with their expo-router equivalents. See https://docs.expo.dev/router/migrate/sdk-55-to-56/ for more information.`, + ], + }; + } +} diff --git a/packages/expo-doctor/src/checks/__tests__/ExpoRouterReactNavigationCheck.test.ts b/packages/expo-doctor/src/checks/__tests__/ExpoRouterReactNavigationCheck.test.ts new file mode 100644 index 00000000000000..19cd6a28407c1b --- /dev/null +++ b/packages/expo-doctor/src/checks/__tests__/ExpoRouterReactNavigationCheck.test.ts @@ -0,0 +1,161 @@ +import { ExpoRouterReactNavigationCheck } from '../ExpoRouterReactNavigationCheck'; + +const additionalProjectProps = { + exp: { + name: 'name', + slug: 'slug', + sdkVersion: '56.0.0', + }, + projectRoot: '/tmp/project', + hasUnusedStaticConfig: false, + staticConfigPath: null, + dynamicConfigPath: null, +}; + +describe('runAsync', () => { + it('returns isSuccessful = true if expo-router is not installed', async () => { + const check = new ExpoRouterReactNavigationCheck(); + const result = await check.runAsync({ + pkg: { + name: 'name', + version: '1.0.0', + dependencies: { '@react-navigation/native': '^7.0.0' }, + }, + ...additionalProjectProps, + }); + expect(result.isSuccessful).toBeTruthy(); + expect(result.issues).toEqual([]); + }); + + it('returns isSuccessful = true if expo-router is installed but no @react-navigation/* package is', async () => { + const check = new ExpoRouterReactNavigationCheck(); + const result = await check.runAsync({ + pkg: { + name: 'name', + version: '1.0.0', + dependencies: { 'expo-router': '^6.0.0' }, + }, + ...additionalProjectProps, + }); + expect(result.isSuccessful).toBeTruthy(); + expect(result.issues).toEqual([]); + }); + + it('returns isSuccessful = false if expo-router and @react-navigation/native are both in dependencies', async () => { + const check = new ExpoRouterReactNavigationCheck(); + const result = await check.runAsync({ + pkg: { + name: 'name', + version: '1.0.0', + dependencies: { + 'expo-router': '^6.0.0', + '@react-navigation/native': '^7.0.0', + }, + }, + ...additionalProjectProps, + }); + expect(result.isSuccessful).toBeFalsy(); + expect(result.issues).toHaveLength(1); + expect(result.issues[0]).toContain('@react-navigation/native'); + expect(result.advice).toHaveLength(1); + }); + + it('returns isSuccessful = false if @react-navigation/* is in devDependencies', async () => { + const check = new ExpoRouterReactNavigationCheck(); + const result = await check.runAsync({ + pkg: { + name: 'name', + version: '1.0.0', + dependencies: { 'expo-router': '^6.0.0' }, + devDependencies: { '@react-navigation/stack': '^7.0.0' }, + }, + ...additionalProjectProps, + }); + expect(result.isSuccessful).toBeFalsy(); + expect(result.issues[0]).toContain('@react-navigation/stack'); + }); + + it('returns isSuccessful = false if expo-router is in devDependencies', async () => { + const check = new ExpoRouterReactNavigationCheck(); + const result = await check.runAsync({ + pkg: { + name: 'name', + version: '1.0.0', + devDependencies: { + 'expo-router': '^6.0.0', + '@react-navigation/native': '^7.0.0', + }, + }, + ...additionalProjectProps, + }); + expect(result.isSuccessful).toBeFalsy(); + expect(result.issues[0]).toContain('@react-navigation/native'); + }); + + it('lists every offending @react-navigation/* package in the message, sorted', async () => { + const check = new ExpoRouterReactNavigationCheck(); + const result = await check.runAsync({ + pkg: { + name: 'name', + version: '1.0.0', + dependencies: { + 'expo-router': '^6.0.0', + '@react-navigation/stack': '^7.0.0', + '@react-navigation/native': '^7.0.0', + '@react-navigation/bottom-tabs': '^7.0.0', + }, + }, + ...additionalProjectProps, + }); + expect(result.isSuccessful).toBeFalsy(); + const message = result.issues[0]; + const nativeIdx = message.indexOf('@react-navigation/native'); + const stackIdx = message.indexOf('@react-navigation/stack'); + const tabsIdx = message.indexOf('@react-navigation/bottom-tabs'); + expect(tabsIdx).toBeGreaterThan(-1); + expect(nativeIdx).toBeGreaterThan(tabsIdx); + expect(stackIdx).toBeGreaterThan(nativeIdx); + }); + + it('ignores peerDependencies (the check targets app projects, not libraries)', async () => { + const check = new ExpoRouterReactNavigationCheck(); + const result = await check.runAsync({ + pkg: { + name: 'name', + version: '1.0.0', + peerDependencies: { + 'expo-router': '^6.0.0', + '@react-navigation/native': '^7.0.0', + }, + }, + ...additionalProjectProps, + }); + expect(result.isSuccessful).toBeTruthy(); + }); + + it('handles same package in both dependencies and devDependencies without duplicating it in the message', async () => { + const check = new ExpoRouterReactNavigationCheck(); + const result = await check.runAsync({ + pkg: { + name: 'name', + version: '1.0.0', + dependencies: { + 'expo-router': '^6.0.0', + '@react-navigation/native': '^7.0.0', + }, + devDependencies: { + '@react-navigation/native': '^7.0.0', + }, + }, + ...additionalProjectProps, + }); + expect(result.isSuccessful).toBeFalsy(); + const occurrences = result.issues[0].match(/@react-navigation\/native/g) ?? []; + expect(occurrences).toHaveLength(1); + }); + + it('has sdkVersionRange = >=56.0.0 <57.0.0', () => { + const check = new ExpoRouterReactNavigationCheck(); + expect(check.sdkVersionRange).toBe('>=56.0.0 <57.0.0'); + }); +}); diff --git a/packages/expo-doctor/src/utils/checkResolver.ts b/packages/expo-doctor/src/utils/checkResolver.ts index f9afddfbc12c6b..e82cc207ff190c 100644 --- a/packages/expo-doctor/src/utils/checkResolver.ts +++ b/packages/expo-doctor/src/utils/checkResolver.ts @@ -13,6 +13,7 @@ import { DependencyVersionOverrideCheck } from '../checks/DependencyVersionOverr import { DirectPackageInstallCheck } from '../checks/DirectPackageInstallCheck'; import { ExpoConfigCommonIssueCheck } from '../checks/ExpoConfigCommonIssueCheck'; import { ExpoConfigSchemaCheck } from '../checks/ExpoConfigSchemaCheck'; +import { ExpoRouterReactNavigationCheck } from '../checks/ExpoRouterReactNavigationCheck'; import { GlobalPackageInstalledLocallyCheck } from '../checks/GlobalPackageInstalledLocallyCheck'; import { IllegalPackageCheck } from '../checks/IllegalPackageCheck'; import { InstalledDependencyVersionCheck } from '../checks/InstalledDependencyVersionCheck'; @@ -52,6 +53,7 @@ export function resolveChecksInScope(exp: ExpoConfig, pkg: PackageJSONConfig): D new GlobalPackageInstalledLocallyCheck(), new DirectPackageInstallCheck(), new PeerDependencyChecks(), + new ExpoRouterReactNavigationCheck(), new AutolinkingDependencyDuplicatesCheck(), new VectorIconsCheck(), From 36e13a5a79461b77c12b1c61a7cb84fb05f3d81d Mon Sep 17 00:00:00 2001 From: Gabriele Scotto di Vettimo <69990484+gsdv@users.noreply.github.com> Date: Tue, 5 May 2026 03:12:19 -0500 Subject: [PATCH 3/3] [docs] Fix SwiftUI DatePicker locale, timeZone, and disabled examples (#45367) # Why Fixes #45305. The SwiftUI `DatePicker` documentation listed `locale`, `timeZone`, and `disabled` as component props on ``. However, `DatePickerProps` (in `packages/expo-ui/src/swift-ui/DatePicker/index.tsx`) doesn't expose any of them. They are actually applied through these modifiers: - `disabled` -> `disabled()` (from `@expo/ui/swift-ui/modifiers`) - `locale` ->`environment('locale', ...)` - `timeZone` -> `environment('timeZone', ...)` Additionally, the `## Custom locale` and `## Custom time zone` sections were also duplicated in the page (each appeared twice). # How - Rewrote the "Disabled picker", "Custom locale", and "Custom time zone" examples to apply the values via the `modifiers` prop, matching the real usage in `apps/native-component-list/src/screens/UI/DatePickerScreen.ios.tsx`. - Removed the duplicated `## Custom locale` and `## Custom time zone` sections. # Test Plan - Ran the docs site locally with `pnpm dev` and verified the DatePicker page renders the updated "Disabled picker", "Custom locale", and "Custom time zone" sections without duplicates and that the code snippets are syntactically valid. # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [x] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- .../sdk/ui/swift-ui/datepicker.mdx | 69 +++---------------- .../v55.0.0/sdk/ui/swift-ui/datepicker.mdx | 69 +++---------------- 2 files changed, 20 insertions(+), 118 deletions(-) diff --git a/docs/pages/versions/unversioned/sdk/ui/swift-ui/datepicker.mdx b/docs/pages/versions/unversioned/sdk/ui/swift-ui/datepicker.mdx index 59cca2a629a96b..38fc11b71aa01d 100644 --- a/docs/pages/versions/unversioned/sdk/ui/swift-ui/datepicker.mdx +++ b/docs/pages/versions/unversioned/sdk/ui/swift-ui/datepicker.mdx @@ -169,9 +169,12 @@ export default function GraphicalDatePickerExample() { ## Disabled picker +You can make the picker non-interactive using the `disabled` modifier. + ```tsx DisabledDatePickerExample.tsx import { useState } from 'react'; import { Host, DatePicker } from '@expo/ui/swift-ui'; +import { disabled } from '@expo/ui/swift-ui/modifiers'; export default function DisabledDatePickerExample() { const [selectedDate, setSelectedDate] = useState(new Date()); @@ -185,61 +188,7 @@ export default function DisabledDatePickerExample() { onDateChange={date => { setSelectedDate(date); }} - disabled - /> - - ); -} -``` - -## Custom locale - -Use the `locale` prop to display the picker in a specific locale. - -```tsx LocaleDatePickerExample.tsx -import { useState } from 'react'; -import { Host, DatePicker } from '@expo/ui/swift-ui'; - -export default function LocaleDatePickerExample() { - const [selectedDate, setSelectedDate] = useState(new Date()); - - return ( - - { - setSelectedDate(date); - }} - locale="fr_FR" - /> - - ); -} -``` - -## Custom time zone - -Use the `timeZone` prop to display the picker in a specific IANA time zone. - -```tsx TimeZoneDatePickerExample.tsx -import { useState } from 'react'; -import { Host, DatePicker } from '@expo/ui/swift-ui'; - -export default function TimeZoneDatePickerExample() { - const [selectedDate, setSelectedDate] = useState(new Date()); - - return ( - - { - setSelectedDate(date); - }} - timeZone="Asia/Tokyo" + modifiers={[disabled()]} /> ); @@ -248,11 +197,12 @@ export default function TimeZoneDatePickerExample() { ## Custom locale -Use the `locale` prop to display the picker in a specific locale. +Apply the `environment` modifier with the `locale` key to display the picker in a specific locale. ```tsx LocaleDatePickerExample.tsx import { useState } from 'react'; import { Host, DatePicker } from '@expo/ui/swift-ui'; +import { environment } from '@expo/ui/swift-ui/modifiers'; export default function LocaleDatePickerExample() { const [selectedDate, setSelectedDate] = useState(new Date()); @@ -266,7 +216,7 @@ export default function LocaleDatePickerExample() { onDateChange={date => { setSelectedDate(date); }} - locale="fr_FR" + modifiers={[environment('locale', 'fr_FR')]} /> ); @@ -275,11 +225,12 @@ export default function LocaleDatePickerExample() { ## Custom time zone -Use the `timeZone` prop to display the picker in a specific IANA time zone. +Apply the `environment` modifier with the `timeZone` key to display the picker in a specific IANA time zone. ```tsx TimeZoneDatePickerExample.tsx import { useState } from 'react'; import { Host, DatePicker } from '@expo/ui/swift-ui'; +import { environment } from '@expo/ui/swift-ui/modifiers'; export default function TimeZoneDatePickerExample() { const [selectedDate, setSelectedDate] = useState(new Date()); @@ -293,7 +244,7 @@ export default function TimeZoneDatePickerExample() { onDateChange={date => { setSelectedDate(date); }} - timeZone="Asia/Tokyo" + modifiers={[environment('timeZone', 'Asia/Tokyo')]} /> ); diff --git a/docs/pages/versions/v55.0.0/sdk/ui/swift-ui/datepicker.mdx b/docs/pages/versions/v55.0.0/sdk/ui/swift-ui/datepicker.mdx index c890e5fd9ac5f3..226e50d5e190f0 100644 --- a/docs/pages/versions/v55.0.0/sdk/ui/swift-ui/datepicker.mdx +++ b/docs/pages/versions/v55.0.0/sdk/ui/swift-ui/datepicker.mdx @@ -169,9 +169,12 @@ export default function GraphicalDatePickerExample() { ## Disabled picker +You can make the picker non-interactive using the `disabled` modifier. + ```tsx DisabledDatePickerExample.tsx import { useState } from 'react'; import { Host, DatePicker } from '@expo/ui/swift-ui'; +import { disabled } from '@expo/ui/swift-ui/modifiers'; export default function DisabledDatePickerExample() { const [selectedDate, setSelectedDate] = useState(new Date()); @@ -185,61 +188,7 @@ export default function DisabledDatePickerExample() { onDateChange={date => { setSelectedDate(date); }} - disabled - /> - - ); -} -``` - -## Custom locale - -Use the `locale` prop to display the picker in a specific locale. - -```tsx LocaleDatePickerExample.tsx -import { useState } from 'react'; -import { Host, DatePicker } from '@expo/ui/swift-ui'; - -export default function LocaleDatePickerExample() { - const [selectedDate, setSelectedDate] = useState(new Date()); - - return ( - - { - setSelectedDate(date); - }} - locale="fr_FR" - /> - - ); -} -``` - -## Custom time zone - -Use the `timeZone` prop to display the picker in a specific IANA time zone. - -```tsx TimeZoneDatePickerExample.tsx -import { useState } from 'react'; -import { Host, DatePicker } from '@expo/ui/swift-ui'; - -export default function TimeZoneDatePickerExample() { - const [selectedDate, setSelectedDate] = useState(new Date()); - - return ( - - { - setSelectedDate(date); - }} - timeZone="Asia/Tokyo" + modifiers={[disabled()]} /> ); @@ -248,11 +197,12 @@ export default function TimeZoneDatePickerExample() { ## Custom locale -Use the `locale` prop to display the picker in a specific locale. +Apply the `environment` modifier with the `locale` key to display the picker in a specific locale. ```tsx LocaleDatePickerExample.tsx import { useState } from 'react'; import { Host, DatePicker } from '@expo/ui/swift-ui'; +import { environment } from '@expo/ui/swift-ui/modifiers'; export default function LocaleDatePickerExample() { const [selectedDate, setSelectedDate] = useState(new Date()); @@ -266,7 +216,7 @@ export default function LocaleDatePickerExample() { onDateChange={date => { setSelectedDate(date); }} - locale="fr_FR" + modifiers={[environment('locale', 'fr_FR')]} /> ); @@ -275,11 +225,12 @@ export default function LocaleDatePickerExample() { ## Custom time zone -Use the `timeZone` prop to display the picker in a specific IANA time zone. +Apply the `environment` modifier with the `timeZone` key to display the picker in a specific IANA time zone. ```tsx TimeZoneDatePickerExample.tsx import { useState } from 'react'; import { Host, DatePicker } from '@expo/ui/swift-ui'; +import { environment } from '@expo/ui/swift-ui/modifiers'; export default function TimeZoneDatePickerExample() { const [selectedDate, setSelectedDate] = useState(new Date()); @@ -293,7 +244,7 @@ export default function TimeZoneDatePickerExample() { onDateChange={date => { setSelectedDate(date); }} - timeZone="Asia/Tokyo" + modifiers={[environment('timeZone', 'Asia/Tokyo')]} /> );