Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
64e12ac
[iOS][calendar][next] Add `presentPicker()` (#44965)
Wenszel May 4, 2026
40e3e0f
[core][Android] Use CT reflection in `recordFromMap` (#45326)
lukmccall May 4, 2026
704c448
chore(babel-preset-expo,metro-config): Re-remove Hermes v1 detection …
kitten May 4, 2026
5da842c
[expo-codemod] Move expo-codemod to packages/expo-codemod (#45324)
Ubax May 4, 2026
0f0d289
[docs] Add SPM instructions to brownfield isolated guide (#45300)
gabrieldonadel May 4, 2026
de80cd1
[core][Android] Use java reflection to convert enums to strings (#45330)
lukmccall May 4, 2026
b54e6e4
chore: bump `@typescript-eslint/*` (#45092)
hassankhan May 4, 2026
a33bc02
[config-plugins] bump google services gradle plugin version (#45320)
vonovak May 4, 2026
2f5c06e
refactor(expo-router,@expo/router-server): replace `TransformStream` …
hassankhan May 4, 2026
17f7e89
[ui] add universal Icon (#45217)
Kudo May 4, 2026
d982d8b
[widgets][android] Add stub methods (#45335)
jakex7 May 4, 2026
c26d01a
[go] use newer eas worker image (#45336)
Kudo May 4, 2026
64104e5
[iOS][calendar][next] Add `calendar.addEventWithForm()` (#44966)
Wenszel May 4, 2026
91c9b3c
refactor(@expo/router-server,expo-server): remove `serializeMetadataT…
hassankhan May 4, 2026
c6cf2d9
[core][Android] Replace `KClass` with `Class` in `ReturnType` (#45338)
lukmccall May 4, 2026
08e089a
[dom-webview] add missing features (#45222)
Kudo May 4, 2026
f31edd0
[dom-webview] add unstable_useExpoModulesBridge (#45223)
Kudo May 4, 2026
c2192d4
[expo][docs] useExpoDOMWebView by default (#45224)
Kudo May 4, 2026
1f3d192
[docs][calendar] Add deprecation notes (#45118)
Wenszel May 4, 2026
8fccdbb
[workspace] Fix pnpm lock file (#45343)
amandeepmittal May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/codemention.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ rules:
mentions: ['amandeepmittal']
- patterns: ['packages/@expo/cli/**']
mentions: ['EvanBacon', 'bycedric', 'kitten']
- patterns: ['packages/@expo/codemod/**']
- patterns: ['packages/expo-codemod/**']
mentions: ['ubax']
- patterns: ['packages/@expo/config/**']
mentions: ['EvanBacon']
Expand Down
4 changes: 2 additions & 2 deletions apps/eas-expo-go/eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"node": "22.17.1",
"credentialsSource": "local",
"android": {
"image": "sdk-54",
"image": "ubuntu-24.04-jdk-17-ndk-r27b-sdk-55",
"resourceClass": "large",
"env": {
"EAS_BUILD_PLATFORM": "android",
Expand All @@ -22,7 +22,7 @@
}
},
"ios": {
"image": "sdk-54",
"image": "macos-sequoia-15.6-xcode-26.2",
"cocoapods": "1.16.2",
"resourceClass": "medium",
"env": {
Expand Down
10 changes: 10 additions & 0 deletions apps/native-component-list/assets/favorite_fill.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,840L422,788Q321,697 255,631Q189,565 150,512.5Q111,460 95.5,416Q80,372 80,326Q80,232 143,169Q206,106 300,106Q352,106 399,128Q446,150 480,190Q514,150 561,128Q608,106 660,106Q754,106 817,169Q880,232 880,326Q880,372 864.5,416Q849,460 810,512.5Q771,565 705,631Q639,697 538,788L480,840Z"/>
</vector>
1 change: 1 addition & 0 deletions apps/native-component-list/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@expo-google-fonts/inter": "^0.2.3",
"@expo/app-integrity": "workspace:*",
"@expo/html-elements": "workspace:*",
"@expo/material-symbols": "~0.1.1",
"@expo/react-native-action-sheet": "^4.1.1",
"@expo/styleguide-base": "^1.0.1",
"@expo/ui": "workspace:*",
Expand Down
17 changes: 15 additions & 2 deletions apps/native-component-list/src/screens/CalendarsNextScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { StackNavigationProp } from '@react-navigation/stack';
import * as Calendar from 'expo-calendar';
import { createCalendar, ExpoCalendar, getCalendars } from 'expo-calendar/next';
import { createCalendar, ExpoCalendar, getCalendars, presentPicker } from 'expo-calendar/next';
import { useState } from 'react';
import { Alert, Platform, ScrollView, StyleSheet, View } from 'react-native';

Expand Down Expand Up @@ -163,7 +163,17 @@ export default function CalendarsNextScreen({ navigation }: { navigation: StackN
if (calendars.length) {
return (
<ScrollView style={styles.container}>
<Button onPress={addCalendar} title="Add New Calendar" />
<Button onPress={addCalendar} title="Add New Calendar" style={styles.topButton} />
{Platform.OS === 'ios' && (
<Button
onPress={async () => {
const picked = await presentPicker();
Alert.alert(picked ? `Picked: ${picked.title}` : 'Cancelled');
}}
title="Present Picker"
style={styles.topButton}
/>
)}
{calendars.map((calendar) => (
<CalendarRow
calendar={calendar}
Expand Down Expand Up @@ -202,4 +212,7 @@ const styles = StyleSheet.create({
calendarRow: {
marginBottom: 12,
},
topButton: {
marginBottom: 8,
},
});
6 changes: 3 additions & 3 deletions apps/native-component-list/src/screens/EventsNextScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { StackScreenProps } from '@react-navigation/stack';
import * as Calendar from 'expo-calendar';
import { ExpoCalendar, ExpoCalendarEvent } from 'expo-calendar/next';
import { AddEventWithFormOptions, ExpoCalendar, ExpoCalendarEvent } from 'expo-calendar/next';
import React, { useState, useEffect } from 'react';
import { Alert, Platform, ScrollView, StyleSheet, Text, View } from 'react-native';

Expand Down Expand Up @@ -229,8 +229,8 @@ const EventsScreen = ({ route }: Props) => {
<Button
onPress={async () => {
const { calendar } = route.params!;
const newEvent = prepareEvent(calendar.id);
const result = calendar.createEvent(newEvent);
const newEvent = prepareEvent(calendar.id, true);
const result = await calendar.addEventWithForm(newEvent as AddEventWithFormOptions);
setTimeout(() => {
Alert.alert('createEventInCalendarAsync result', JSON.stringify(result), undefined, {
cancelable: true,
Expand Down
121 changes: 121 additions & 0 deletions apps/native-component-list/src/screens/UIUniversal/IconScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Column, Host, Icon, Row, ScrollView, Text } from '@expo/ui';
import * as React from 'react';

const TITLE_STYLE = { fontSize: 16, fontWeight: '600' as const };
const DESCRIPTION_STYLE = { fontSize: 13, color: '#666' };

// Prefer the `import()` form for the Android side — TypeScript validates the
// literal path through `@expo/material-symbols`'s exports map, so typos surface
// at compile time. The `@expo/ui/babel-plugin` (auto-loaded by
// `babel-preset-expo`) rewrites `import('...')` to `require('...')` so Metro
// still tree-shakes the unused branch per platform.
const STAR = Icon.select({
ios: 'star.fill',
android: import('@expo/material-symbols/star.xml'),
});

const HOME = Icon.select({
ios: 'house.fill',
android: import('@expo/material-symbols/home.xml'),
});

const HEART = Icon.select({
ios: 'heart.fill',
android: import('@expo/material-symbols/favorite.xml'),
});

export default function IconScreen() {
const [favorited, setFavorited] = React.useState(false);

return (
<Host style={{ flex: 1 }}>
<ScrollView style={{ padding: 16 }}>
<Column spacing={24}>
<Column spacing={8}>
<Text textStyle={TITLE_STYLE}>Icon.select (recommended)</Text>
<Text textStyle={DESCRIPTION_STYLE}>
Hoisted module-level definitions. The babel-preset-expo plugin rewrites the call into
a Platform.OS ternary so the unused side is stripped per platform.
</Text>
<Row spacing={16}>
<Icon name={STAR} size={28} color="#FFB400" />
<Icon name={HOME} size={28} color="#3478F6" />
<Icon name={HEART} size={28} color="#FF3B30" />
</Row>
</Column>

<Column spacing={8}>
<Text textStyle={TITLE_STYLE}>Inline Icon.select</Text>
<Text textStyle={DESCRIPTION_STYLE}>
Defined at the call site — also tree-shakes via the babel plugin.
</Text>
<Icon
name={Icon.select({
ios: 'gearshape.fill',
android: import('@expo/material-symbols/settings.xml'),
})}
size={28}
/>
</Column>

<Column spacing={8}>
<Text textStyle={TITLE_STYLE}>{'Inline { ios, android } object'}</Text>
<Text textStyle={DESCRIPTION_STYLE}>
Same runtime behavior as Icon.select but does NOT tree-shake — both sides ship to both
platforms. Use Icon.select instead when bundle size matters.
</Text>
<Icon
name={{
ios: 'bell.fill',
android: require('@expo/material-symbols/notifications.xml'),
}}
size={28}
color="#34C759"
/>
</Column>

<Column spacing={8}>
<Text textStyle={TITLE_STYLE}>State-driven icon</Text>
<Text textStyle={DESCRIPTION_STYLE}>
Tap to toggle. iOS swaps the SF Symbol between filled and outlined. Android pairs
`@expo/material-symbols/favorite.xml` (outlined, from the package) with a local custom
XML drawable for the filled variant — drop any vector XML from fonts.google.com or
`npx add-icon --fill` into your project and `require()` it.
</Text>
<Icon
name={
favorited
? Icon.select({
ios: 'heart.fill',
android: require('../../../assets/favorite_fill.xml'),
})
: Icon.select({
ios: 'heart',
android: import('@expo/material-symbols/favorite.xml'),
})
}
size={28}
color={favorited ? '#FF3B30' : '#8E8E93'}
onPress={() => setFavorited((v) => !v)}
accessibilityLabel={favorited ? 'Unfavorite' : 'Favorite'}
/>
</Column>

<Column spacing={8}>
<Text textStyle={TITLE_STYLE}>Sized + tinted variants</Text>
<Row alignment="center" spacing={12}>
<Icon name={STAR} size={16} color="#FFB400" />
<Icon name={STAR} size={24} color="#FFB400" />
<Icon name={STAR} size={32} color="#FFB400" />
<Icon name={STAR} size={40} color="#FFB400" />
</Row>
</Column>
</Column>
</ScrollView>
</Host>
);
}

IconScreen.navigationOptions = {
title: 'Universal Icon',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Column, Text } from '@expo/ui';

export default function IconScreen() {
return (
<Column spacing={8} alignment="center" style={{ padding: 24 }}>
<Text textStyle={{ fontSize: 16, fontWeight: '600' }}>Icon is not supported on web</Text>
<Text textStyle={{ fontSize: 13, color: '#666', textAlign: 'center' }}>
{
'The universal `Icon` component bridges SF Symbols on iOS and `@expo/material-symbols` XML drawables on Android. There is no web rendering — `<Icon>` returns null. Run this screen on an iOS or Android device to see the examples.'
}
</Text>
</Column>
);
}

IconScreen.navigationOptions = {
title: 'Universal Icon',
};
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ export const UIUniversalScreens = [
return optionalRequire(() => require('./SpacerScreen'));
},
},
{
name: `${SCREEN_NAME_PREFIX}Icon`,
route: 'ui-universal/icon',
options: { title: 'Icon' },
getComponent() {
return optionalRequire(() => require('./IconScreen'));
},
},
];

function stripPrefix(elements: ListElement[]): ListElement[] {
Expand Down
8 changes: 7 additions & 1 deletion apps/router-e2e/__e2e__/05-use-dom/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,13 @@ export default function Page() {
</TestCase>

<TestCase name="NativeModuleProxy">
<NativeModuleProxy dom={{ matchContents: true, useExpoDOMWebView: true }} />
<NativeModuleProxy
dom={{
matchContents: true,
useExpoDOMWebView: true,
unstable_useExpoModulesBridge: true,
}}
/>
</TestCase>
<TestCase name="Router">
<RouterDemo
Expand Down
19 changes: 16 additions & 3 deletions apps/router-e2e/__e2e__/server-loader/app/meta.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { useLoaderData, useLocalSearchParams, usePathname } from 'expo-router';
import Head from 'expo-router/head';
import type { Metadata } from 'expo-router/server';
import { Suspense } from 'react';

import { Container } from '../components/Container';
import { Loading } from '../components/Loading';
import { Table, TableRow } from '../components/Table';
import { SiteLinks, SiteLink } from '../components/SiteLink';

// Tests metadata when using streaming SSR
export function generateMetadata() {
return {
title: 'Meta page',
description: 'Meta tag testing',
keywords: 'expo-router,loaders,meta',
authors: {
name: 'Expo',
},
} satisfies Metadata;
}

export async function loader() {
return {
title: 'Meta page',
description: 'Meta tag testing',
keywords: 'expo-router,loaders,meta',
author: 'Expo'
author: 'Expo',
};
}

Expand All @@ -31,6 +43,7 @@ const MetaScreen = () => {

return (
<>
{/* Tests metadata when using SSG */}
<Head>
<title>{data.title}</title>
<meta name="description" content={data.description} />
Expand All @@ -50,4 +63,4 @@ const MetaScreen = () => {
</SiteLinks>
</>
);
}
};
12 changes: 9 additions & 3 deletions apps/router-e2e/__e2e__/static-rendering/app/+html.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// Test the custom HTML component is rendered during SSR.

import { usePathname } from 'expo-router';
import { ScrollViewStyleReset } from 'expo-router/html';
import { ScrollViewStyleReset, useServerDocumentContext } from 'expo-router/html';

export default function Html({ children }) {
const { bodyAttributes, bodyNodes, htmlAttributes, headNodes } = useServerDocumentContext();
// Test that this is defined and works during SSR.
const pathname = usePathname();

return (
<html lang="en">
<html lang="en" {...htmlAttributes}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
Expand All @@ -21,8 +22,13 @@ export default function Html({ children }) {
{/* Test that server-only env vars are exposed as this file is a server file. */}
<meta name="expo-e2e-private-env-var" content={process.env.EXPO_NOT_PUBLIC_TEST_VALUE} />
<ScrollViewStyleReset />

{headNodes}
</head>
<body>{children}</body>
<body {...bodyAttributes}>
{children}
{bodyNodes}
</body>
</html>
);
}
Loading
Loading