From 5947475a4e006a02cb9cd7d34e796ed3572b2f6a Mon Sep 17 00:00:00 2001 From: Kota <128109461+kota113@users.noreply.github.com> Date: Wed, 27 May 2026 16:45:50 +1000 Subject: [PATCH 1/7] [expo-ui] feat: add jetpack `NavigationBar` (#46217) # Why Add Material 3 `NavigationBar` support to `@expo/ui` Jetpack Compose so Android apps can build native bottom navigation using Expo UI primitives, which is equivalent to `TabView` in Swift UI. # How Implemented `NavigationBar` and `NavigationBarItem` for Jetpack Compose. - Added JS exports for `NavigationBar` / `NavigationBarItem`. - Added Android Compose bindings backed by Material 3 `NavigationBar` and `NavigationBarItem`. - Added API docs and generated docs API data. - Added a `native-component-list` demo screen. - Added a changelog entry. # Test Plan Ran successfully: ```sh pnpm build pnpm lint ./gradlew :expo-ui:compileDebugKotlin node tools/bin/expotools.js gdad -p expo-ui/jetpack-compose/navigationbar pnpm --filter native-component-list tsc ``` ```sh ./gradlew assembleDebug ./gradlew assembleRelease ``` # Checklist - [x] 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) - [x] 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) --- .../UI/NavigationBarScreen.android.tsx | 69 +++++++ .../src/screens/UI/UIScreen.android.tsx | 8 + .../sdk/ui/jetpack-compose/navigationbar.mdx | 91 +++++++++ .../sdk/ui/jetpack-compose/navigationbar.mdx | 91 +++++++++ .../jetpack-compose/navigationbar.json | 1 + .../expo-ui/navigationbar/android-dark.webp | Bin 0 -> 5446 bytes .../expo-ui/navigationbar/android-light.webp | Bin 0 -> 5138 bytes packages/expo-ui/CHANGELOG.md | 2 + .../main/java/expo/modules/ui/ExpoUIModule.kt | 14 ++ .../java/expo/modules/ui/NavigationBarView.kt | 95 ++++++++++ .../jetpack-compose/NavigationBar/index.d.ts | 101 ++++++++++ .../NavigationBar/index.d.ts.map | 1 + .../expo-ui/build/jetpack-compose/index.d.ts | 1 + .../build/jetpack-compose/index.d.ts.map | 2 +- .../jetpack-compose/NavigationBar/index.tsx | 174 ++++++++++++++++++ packages/expo-ui/src/jetpack-compose/index.ts | 1 + tools/src/commands/GenerateDocsAPIData.ts | 1 + 17 files changed, 651 insertions(+), 1 deletion(-) create mode 100644 apps/native-component-list/src/screens/UI/NavigationBarScreen.android.tsx create mode 100644 docs/pages/versions/unversioned/sdk/ui/jetpack-compose/navigationbar.mdx create mode 100644 docs/pages/versions/v56.0.0/sdk/ui/jetpack-compose/navigationbar.mdx create mode 100644 docs/public/static/data/unversioned/expo-ui/jetpack-compose/navigationbar.json create mode 100644 docs/public/static/images/expo-ui/navigationbar/android-dark.webp create mode 100644 docs/public/static/images/expo-ui/navigationbar/android-light.webp create mode 100644 packages/expo-ui/android/src/main/java/expo/modules/ui/NavigationBarView.kt create mode 100644 packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts create mode 100644 packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts.map create mode 100644 packages/expo-ui/src/jetpack-compose/NavigationBar/index.tsx diff --git a/apps/native-component-list/src/screens/UI/NavigationBarScreen.android.tsx b/apps/native-component-list/src/screens/UI/NavigationBarScreen.android.tsx new file mode 100644 index 00000000000000..47d63b4d3b8d9d --- /dev/null +++ b/apps/native-component-list/src/screens/UI/NavigationBarScreen.android.tsx @@ -0,0 +1,69 @@ +import { + Box, + Column, + Host, + Icon, + NavigationBar, + NavigationBarItem, + Surface, + Text as ComposeText, +} from '@expo/ui/jetpack-compose'; +import { align, fillMaxSize, fillMaxWidth, padding } from '@expo/ui/jetpack-compose/modifiers'; +import * as React from 'react'; + +const mailIcon = require('../../../assets/icons/ui/mail.xml'); +const personIcon = require('../../../assets/icons/ui/person.xml'); +const wifiIcon = require('../../../assets/icons/ui/wifi.xml'); + +const tabs = [ + { key: 'mail', label: 'Mail', icon: mailIcon }, + { key: 'profile', label: 'Profile', icon: personIcon }, + { key: 'network', label: 'Network', icon: wifiIcon }, +] as const; + +export default function NavigationBarScreen() { + const [selectedTab, setSelectedTab] = React.useState<(typeof tabs)[number]['key']>('mail'); + const selected = tabs.find((tab) => tab.key === selectedTab) ?? tabs[0]; + + return ( + + + + + + {selected.label} + + Tap a destination in the navigation bar. + + + + + + {tabs.map((tab) => ( + setSelectedTab(tab.key)}> + + + + + + + + {tab.label} + + + ))} + + + + ); +} + +NavigationBarScreen.navigationOptions = { + title: 'NavigationBar', +}; diff --git a/apps/native-component-list/src/screens/UI/UIScreen.android.tsx b/apps/native-component-list/src/screens/UI/UIScreen.android.tsx index 411d33a21a0455..42adb17f3f11ec 100644 --- a/apps/native-component-list/src/screens/UI/UIScreen.android.tsx +++ b/apps/native-component-list/src/screens/UI/UIScreen.android.tsx @@ -266,6 +266,14 @@ export const UIScreens = [ return optionalRequire(() => require('./ListScreen')); }, }, + { + name: 'NavigationBar component', + route: 'ui/navigation-bar', + options: {}, + getComponent() { + return optionalRequire(() => require('./NavigationBarScreen')); + }, + }, { name: 'BottomSheet component', route: 'ui/bottomsheet', diff --git a/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/navigationbar.mdx b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/navigationbar.mdx new file mode 100644 index 00000000000000..391b6eec0ce33e --- /dev/null +++ b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/navigationbar.mdx @@ -0,0 +1,91 @@ +--- +title: NavigationBar +description: A Jetpack Compose NavigationBar component for Material 3 bottom navigation. +sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-ui' +packageName: '@expo/ui' +platforms: ['android', 'expo-go'] +--- + +import APISection from '~/components/plugins/APISection'; +import { APIInstallSection } from '~/components/plugins/InstallSection'; +import { ContentSpotlight } from '~/ui/components/ContentSpotlight'; + +Expo UI NavigationBar matches the official Jetpack Compose [`NavigationBar`](https://developer.android.com/develop/ui/compose/components/navigation-bar) API. It displays a row of destinations for switching between top-level app sections. + + + +## Installation + + + +## Usage + +### Basic navigation bar + +Manage the selected item in React state and pass `selected` to each `NavigationBarItem`. + +```tsx BasicNavigationBar.tsx +import { useState } from 'react'; +import { Host, Icon, NavigationBar, NavigationBarItem, Text } from '@expo/ui/jetpack-compose'; + +const HOME_ICON = require('./assets/home.xml'); +const SEARCH_ICON = require('./assets/search.xml'); +const SETTINGS_ICON = require('./assets/settings.xml'); + +export default function BasicNavigationBar() { + const [selectedTab, setSelectedTab] = useState('home'); + + return ( + + + setSelectedTab('home')}> + + + + + Home + + + + setSelectedTab('search')}> + + + + + Search + + + + setSelectedTab('settings')}> + + + + + Settings + + + + + ); +} +``` + +## API + +```tsx +import { NavigationBar, NavigationBarItem } from '@expo/ui/jetpack-compose'; +``` + + + + diff --git a/docs/pages/versions/v56.0.0/sdk/ui/jetpack-compose/navigationbar.mdx b/docs/pages/versions/v56.0.0/sdk/ui/jetpack-compose/navigationbar.mdx new file mode 100644 index 00000000000000..391b6eec0ce33e --- /dev/null +++ b/docs/pages/versions/v56.0.0/sdk/ui/jetpack-compose/navigationbar.mdx @@ -0,0 +1,91 @@ +--- +title: NavigationBar +description: A Jetpack Compose NavigationBar component for Material 3 bottom navigation. +sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-ui' +packageName: '@expo/ui' +platforms: ['android', 'expo-go'] +--- + +import APISection from '~/components/plugins/APISection'; +import { APIInstallSection } from '~/components/plugins/InstallSection'; +import { ContentSpotlight } from '~/ui/components/ContentSpotlight'; + +Expo UI NavigationBar matches the official Jetpack Compose [`NavigationBar`](https://developer.android.com/develop/ui/compose/components/navigation-bar) API. It displays a row of destinations for switching between top-level app sections. + + + +## Installation + + + +## Usage + +### Basic navigation bar + +Manage the selected item in React state and pass `selected` to each `NavigationBarItem`. + +```tsx BasicNavigationBar.tsx +import { useState } from 'react'; +import { Host, Icon, NavigationBar, NavigationBarItem, Text } from '@expo/ui/jetpack-compose'; + +const HOME_ICON = require('./assets/home.xml'); +const SEARCH_ICON = require('./assets/search.xml'); +const SETTINGS_ICON = require('./assets/settings.xml'); + +export default function BasicNavigationBar() { + const [selectedTab, setSelectedTab] = useState('home'); + + return ( + + + setSelectedTab('home')}> + + + + + Home + + + + setSelectedTab('search')}> + + + + + Search + + + + setSelectedTab('settings')}> + + + + + Settings + + + + + ); +} +``` + +## API + +```tsx +import { NavigationBar, NavigationBarItem } from '@expo/ui/jetpack-compose'; +``` + + + + diff --git a/docs/public/static/data/unversioned/expo-ui/jetpack-compose/navigationbar.json b/docs/public/static/data/unversioned/expo-ui/jetpack-compose/navigationbar.json new file mode 100644 index 00000000000000..36c82c83a614e9 --- /dev/null +++ b/docs/public/static/data/unversioned/expo-ui/jetpack-compose/navigationbar.json @@ -0,0 +1 @@ +{"schemaVersion":"2.0","name":"expo-ui/jetpack-compose/navigationbar","variant":"project","kind":1,"children":[{"name":"NavigationBarItemColors","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Colors for navigation bar items in different states."}]},"children":[{"name":"disabledIconColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"disabledTextColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"selectedIconColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"selectedIndicatorColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"selectedTextColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"unselectedIconColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"unselectedTextColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}}]},{"name":"NavigationBarItemProps","variant":"declaration","kind":2097152,"children":[{"name":"alwaysShowLabel","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to always show the label."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"children","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Children containing "},{"kind":"code","text":"`Icon`"},{"kind":"text","text":", "},{"kind":"code","text":"`SelectedIcon`"},{"kind":"text","text":", and "},{"kind":"code","text":"`Label`"},{"kind":"text","text":" slots."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"React.ReactNode","package":"@types/react"}},{"name":"colors","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Colors for the item in different states."}]},"type":{"type":"reference","name":"NavigationBarItemColors","package":"@expo/ui"}},{"name":"enabled","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the item is enabled."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"true"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Modifiers for the component."}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}},{"name":"onClick","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Callback that is called when the item is clicked."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"type":{"type":"intrinsic","name":"void"}}]}}},{"name":"selected","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Whether this item is currently selected."}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"NavigationBarProps","variant":"declaration","kind":2097152,"children":[{"name":"children","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Navigation bar items."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"React.ReactNode","package":"@types/react"}},{"name":"containerColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Background color of the navigation bar."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"NavigationBarDefaults.containerColor"}]}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"contentColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Preferred content color inside the navigation bar."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"contentColorFor(containerColor)"}]}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Modifiers for the component."}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}},{"name":"tonalElevation","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Tonal elevation in dp."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"NavigationBarDefaults.Elevation"}]}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"NavigationBar","variant":"declaration","kind":64,"signatures":[{"name":"NavigationBar","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"A Material Design 3 navigation bar."}]},"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","name":"NavigationBarProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]},{"name":"NavigationBarItem","variant":"declaration","kind":64,"children":[{"name":"Icon","variant":"declaration","kind":1024,"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Icon slot for "},{"kind":"code","text":"`NavigationBarItem`"},{"kind":"text","text":"."}]},"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/jetpack-compose/NavigationBar/index.tsx","qualifiedName":"SlotProps"},"name":"SlotProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]}}},{"name":"Label","variant":"declaration","kind":1024,"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Label slot for "},{"kind":"code","text":"`NavigationBarItem`"},{"kind":"text","text":"."}]},"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/jetpack-compose/NavigationBar/index.tsx","qualifiedName":"SlotProps"},"name":"SlotProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]}}},{"name":"SelectedIcon","variant":"declaration","kind":1024,"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Selected icon slot for "},{"kind":"code","text":"`NavigationBarItem`"},{"kind":"text","text":". Falls back to "},{"kind":"code","text":"`Icon`"},{"kind":"text","text":" when omitted."}]},"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/jetpack-compose/NavigationBar/index.tsx","qualifiedName":"SlotProps"},"name":"SlotProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]}}}],"signatures":[{"name":"NavigationBarItem","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"A Material Design 3 navigation bar item. Must be used inside "},{"kind":"code","text":"`NavigationBar`"},{"kind":"text","text":"."}]},"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","name":"NavigationBarItemProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]}],"packageName":"@expo/ui"} \ No newline at end of file diff --git a/docs/public/static/images/expo-ui/navigationbar/android-dark.webp b/docs/public/static/images/expo-ui/navigationbar/android-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..ffd0951865fa8a8e8243cdb894b3f13979c09768 GIT binary patch literal 5446 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 for custom label style. ([#46288](https://github.com/expo/expo/pull/46288) by [@kudo](https://github.com/kudo)) - [universal] Added `` for custom label style. ([#46288](https://github.com/expo/expo/pull/46288) by [@kudo](https://github.com/kudo)) ### 🐛 Bug fixes + ### 💡 Others - [universal] Revamp web universal components (`Button`, `Checkbox`, `Picker`, `Slider`,`Switch`,`TextInput`,) with shared design tokens, light / dark themes, and keyboard focus styles. ([#46258](https://github.com/expo/expo/pull/46258) by [@zoontek](https://github.com/zoontek)) diff --git a/packages/expo-ui/android/src/main/java/expo/modules/ui/ExpoUIModule.kt b/packages/expo-ui/android/src/main/java/expo/modules/ui/ExpoUIModule.kt index 4d8a2953f2fcaf..573dc4fe9fa92c 100644 --- a/packages/expo-ui/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +++ b/packages/expo-ui/android/src/main/java/expo/modules/ui/ExpoUIModule.kt @@ -610,6 +610,20 @@ class ExpoUIModule : Module() { } } + ExpoUIView("NavigationBarView") { + Content { props -> + NavigationBarContent(props) + } + } + + ExpoUIView("NavigationBarItemView") { + val onButtonPressed by Event() + + Content { props -> + NavigationBarItemContent(props) { onButtonPressed(Unit) } + } + } + ExpoUIView("SpacerView") { Content { props -> SpacerContent(props) diff --git a/packages/expo-ui/android/src/main/java/expo/modules/ui/NavigationBarView.kt b/packages/expo-ui/android/src/main/java/expo/modules/ui/NavigationBarView.kt new file mode 100644 index 00000000000000..83101fb1411db4 --- /dev/null +++ b/packages/expo-ui/android/src/main/java/expo/modules/ui/NavigationBarView.kt @@ -0,0 +1,95 @@ +package expo.modules.ui + +import android.graphics.Color +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color as ComposeColor +import androidx.compose.ui.unit.dp +import expo.modules.kotlin.records.Field +import expo.modules.kotlin.records.Record +import expo.modules.kotlin.types.OptimizedRecord +import expo.modules.kotlin.views.ComposeProps +import expo.modules.kotlin.views.FunctionalComposableScope +import expo.modules.kotlin.views.OptimizedComposeProps + +@OptimizedRecord +data class NavigationBarItemColors( + @Field val selectedIconColor: Color? = null, + @Field val selectedTextColor: Color? = null, + @Field val selectedIndicatorColor: Color? = null, + @Field val unselectedIconColor: Color? = null, + @Field val unselectedTextColor: Color? = null, + @Field val disabledIconColor: Color? = null, + @Field val disabledTextColor: Color? = null +) : Record + +@OptimizedComposeProps +data class NavigationBarProps( + val containerColor: Color? = null, + val contentColor: Color? = null, + val tonalElevation: Float? = null, + val modifiers: ModifierList = emptyList() +) : ComposeProps + +@OptimizedComposeProps +data class NavigationBarItemProps( + val selected: Boolean = false, + val enabled: Boolean = true, + val alwaysShowLabel: Boolean = true, + val colors: NavigationBarItemColors = NavigationBarItemColors(), + val modifiers: ModifierList = emptyList() +) : ComposeProps + +@Composable +fun FunctionalComposableScope.NavigationBarContent(props: NavigationBarProps) { + val resolvedContainerColor = props.containerColor.composeOrNull ?: NavigationBarDefaults.containerColor + val modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher) + + NavigationBar( + modifier = modifier, + containerColor = resolvedContainerColor, + contentColor = props.contentColor.composeOrNull ?: contentColorFor(resolvedContainerColor), + tonalElevation = props.tonalElevation?.dp ?: NavigationBarDefaults.Elevation + ) { + Children(UIComposableScope(rowScope = this@NavigationBar), filter = { !isSlotView(it) }) + } +} + +@Composable +fun FunctionalComposableScope.NavigationBarItemContent( + props: NavigationBarItemProps, + onClick: () -> Unit +) { + val iconSlotView = findChildSlotView(view, "icon") + val labelSlotView = findChildSlotView(view, "label") + val modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher) + val label: (@Composable () -> Unit)? = labelSlotView?.let { slot -> { slot.renderSlot() } } + val rowScope = composableScope.rowScope ?: return + + with(rowScope) { + NavigationBarItem( + selected = props.selected, + onClick = onClick, + icon = { + iconSlotView?.renderSlot() + }, + modifier = modifier, + enabled = props.enabled, + label = label, + alwaysShowLabel = props.alwaysShowLabel, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = props.colors.selectedIconColor.composeOrNull ?: ComposeColor.Unspecified, + selectedTextColor = props.colors.selectedTextColor.composeOrNull ?: ComposeColor.Unspecified, + indicatorColor = props.colors.selectedIndicatorColor.composeOrNull ?: ComposeColor.Unspecified, + unselectedIconColor = props.colors.unselectedIconColor.composeOrNull ?: ComposeColor.Unspecified, + unselectedTextColor = props.colors.unselectedTextColor.composeOrNull ?: ComposeColor.Unspecified, + disabledIconColor = props.colors.disabledIconColor.composeOrNull ?: ComposeColor.Unspecified, + disabledTextColor = props.colors.disabledTextColor.composeOrNull ?: ComposeColor.Unspecified + ) + ) + } +} diff --git a/packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts b/packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts new file mode 100644 index 00000000000000..e84f86222a7eb5 --- /dev/null +++ b/packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts @@ -0,0 +1,101 @@ +import { type ColorValue } from 'react-native'; +import { type ModifierConfig } from '../../types'; +type SlotProps = { + children: React.ReactNode; +}; +/** + * Colors for navigation bar items in different states. + */ +export type NavigationBarItemColors = { + selectedIconColor?: ColorValue; + selectedTextColor?: ColorValue; + selectedIndicatorColor?: ColorValue; + unselectedIconColor?: ColorValue; + unselectedTextColor?: ColorValue; + disabledIconColor?: ColorValue; + disabledTextColor?: ColorValue; +}; +export type NavigationBarProps = { + /** + * Background color of the navigation bar. + * @default NavigationBarDefaults.containerColor + */ + containerColor?: ColorValue; + /** + * Preferred content color inside the navigation bar. + * @default contentColorFor(containerColor) + */ + contentColor?: ColorValue; + /** + * Tonal elevation in dp. + * @default NavigationBarDefaults.Elevation + */ + tonalElevation?: number; + /** + * Modifiers for the component. + */ + modifiers?: ModifierConfig[]; + /** + * Navigation bar items. + */ + children?: React.ReactNode; +}; +export type NavigationBarItemProps = { + /** + * Whether this item is currently selected. + */ + selected: boolean; + /** + * Callback that is called when the item is clicked. + */ + onClick?: () => void; + /** + * Whether the item is enabled. + * @default true + */ + enabled?: boolean; + /** + * Whether to always show the label. + * @default true + */ + alwaysShowLabel?: boolean; + /** + * Colors for the item in different states. + */ + colors?: NavigationBarItemColors; + /** + * Modifiers for the component. + */ + modifiers?: ModifierConfig[]; + /** + * Children containing `Icon`, `SelectedIcon`, and `Label` slots. + */ + children?: React.ReactNode; +}; +/** + * Icon slot for `NavigationBarItem`. + */ +declare function NavigationBarItemIcon(props: SlotProps): import("react/jsx-runtime").JSX.Element; +/** + * Selected icon slot for `NavigationBarItem`. Falls back to `Icon` when omitted. + */ +declare function NavigationBarItemSelectedIcon(props: SlotProps): import("react/jsx-runtime").JSX.Element; +/** + * Label slot for `NavigationBarItem`. + */ +declare function NavigationBarItemLabel(props: SlotProps): import("react/jsx-runtime").JSX.Element; +/** + * A Material Design 3 navigation bar. + */ +export declare function NavigationBar(props: NavigationBarProps): import("react/jsx-runtime").JSX.Element; +/** + * A Material Design 3 navigation bar item. Must be used inside `NavigationBar`. + */ +declare function NavigationBarItemComponent(props: NavigationBarItemProps): import("react/jsx-runtime").JSX.Element; +declare namespace NavigationBarItemComponent { + var Icon: typeof NavigationBarItemIcon; + var SelectedIcon: typeof NavigationBarItemSelectedIcon; + var Label: typeof NavigationBarItemLabel; +} +export { NavigationBarItemComponent as NavigationBarItem }; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts.map b/packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts.map new file mode 100644 index 00000000000000..4b9f3a464c3653 --- /dev/null +++ b/packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/jetpack-compose/NavigationBar/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,KAAK,cAAc,EAAkB,MAAM,aAAa,CAAC;AAGlE,KAAK,SAAS,GAAG;IACf,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAOF;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,CAAC,EAAE,UAAU,CAAC;IAC/B,iBAAiB,CAAC,EAAE,UAAU,CAAC;IAC/B,sBAAsB,CAAC,EAAE,UAAU,CAAC;IACpC,mBAAmB,CAAC,EAAE,UAAU,CAAC;IACjC,mBAAmB,CAAC,EAAE,UAAU,CAAC;IACjC,iBAAiB,CAAC,EAAE,UAAU,CAAC;IAC/B,iBAAiB,CAAC,EAAE,UAAU,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;OAGG;IACH,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;IAC7B;;OAEG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;OAEG;IACH,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC;;OAEG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;IAC7B;;OAEG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,CAAC;AAuCF;;GAEG;AACH,iBAAS,qBAAqB,CAAC,KAAK,EAAE,SAAS,2CAE9C;AAED;;GAEG;AACH,iBAAS,6BAA6B,CAAC,KAAK,EAAE,SAAS,2CAEtD;AAED;;GAEG;AACH,iBAAS,sBAAsB,CAAC,KAAK,EAAE,SAAS,2CAE/C;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,2CAOtD;AAED;;GAEG;AACH,iBAAS,0BAA0B,CAAC,KAAK,EAAE,sBAAsB,2CAOhE;kBAPQ,0BAA0B;;;;;AAanC,OAAO,EAAE,0BAA0B,IAAI,iBAAiB,EAAE,CAAC"} \ No newline at end of file diff --git a/packages/expo-ui/build/jetpack-compose/index.d.ts b/packages/expo-ui/build/jetpack-compose/index.d.ts index 1046f05fef6b24..d967890d40437c 100644 --- a/packages/expo-ui/build/jetpack-compose/index.d.ts +++ b/packages/expo-ui/build/jetpack-compose/index.d.ts @@ -29,6 +29,7 @@ export { TextField, OutlinedTextField, type TextFieldProps, type TextFieldRef, t export * from './ToggleButton'; export * from './Shape'; export * from './ModalBottomSheet'; +export * from './NavigationBar'; export * from './Carousel'; export { HorizontalPager, type HorizontalPagerHandle, type HorizontalPagerProps, } from './HorizontalPager'; export * from './SearchBar'; diff --git a/packages/expo-ui/build/jetpack-compose/index.d.ts.map b/packages/expo-ui/build/jetpack-compose/index.d.ts.map index 9523984cfd4b5f..48109c840de453 100644 --- a/packages/expo-ui/build/jetpack-compose/index.d.ts.map +++ b/packages/expo-ui/build/jetpack-compose/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/jetpack-compose/index.ts"],"names":[],"mappings":"AAAA,OAAO,uCAAuC,CAAC;AAE/C,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAClF,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,KAAK,SAAS,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9C,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AAEnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,OAAO,CAAC;AACtB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/jetpack-compose/index.ts"],"names":[],"mappings":"AAAA,OAAO,uCAAuC,CAAC;AAE/C,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAClF,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,KAAK,SAAS,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9C,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AAEnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,OAAO,CAAC;AACtB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC"} \ No newline at end of file diff --git a/packages/expo-ui/src/jetpack-compose/NavigationBar/index.tsx b/packages/expo-ui/src/jetpack-compose/NavigationBar/index.tsx new file mode 100644 index 00000000000000..321166d82e43b3 --- /dev/null +++ b/packages/expo-ui/src/jetpack-compose/NavigationBar/index.tsx @@ -0,0 +1,174 @@ +import { requireNativeView } from 'expo'; +import { type ColorValue } from 'react-native'; + +import { type ModifierConfig, type ViewEvent } from '../../types'; +import { createViewModifierEventListener } from '../modifiers'; + +type SlotProps = { + children: React.ReactNode; +}; + +type NativeSlotViewProps = { + slotName: string; + children: React.ReactNode; +}; + +/** + * Colors for navigation bar items in different states. + */ +export type NavigationBarItemColors = { + selectedIconColor?: ColorValue; + selectedTextColor?: ColorValue; + selectedIndicatorColor?: ColorValue; + unselectedIconColor?: ColorValue; + unselectedTextColor?: ColorValue; + disabledIconColor?: ColorValue; + disabledTextColor?: ColorValue; +}; + +export type NavigationBarProps = { + /** + * Background color of the navigation bar. + * @default NavigationBarDefaults.containerColor + */ + containerColor?: ColorValue; + /** + * Preferred content color inside the navigation bar. + * @default contentColorFor(containerColor) + */ + contentColor?: ColorValue; + /** + * Tonal elevation in dp. + * @default NavigationBarDefaults.Elevation + */ + tonalElevation?: number; + /** + * Modifiers for the component. + */ + modifiers?: ModifierConfig[]; + /** + * Navigation bar items. + */ + children?: React.ReactNode; +}; + +export type NavigationBarItemProps = { + /** + * Whether this item is currently selected. + */ + selected: boolean; + /** + * Callback that is called when the item is clicked. + */ + onClick?: () => void; + /** + * Whether the item is enabled. + * @default true + */ + enabled?: boolean; + /** + * Whether to always show the label. + * @default true + */ + alwaysShowLabel?: boolean; + /** + * Colors for the item in different states. + */ + colors?: NavigationBarItemColors; + /** + * Modifiers for the component. + */ + modifiers?: ModifierConfig[]; + /** + * Children containing `Icon`, `SelectedIcon`, and `Label` slots. + */ + children?: React.ReactNode; +}; + +type NativeNavigationBarItemProps = Omit & + ViewEvent<'onButtonPressed', void>; + +const NavigationBarNativeView: React.ComponentType = requireNativeView( + 'ExpoUI', + 'NavigationBarView' +); + +const NavigationBarItemNativeView: React.ComponentType = + requireNativeView('ExpoUI', 'NavigationBarItemView'); + +const SlotNativeView: React.ComponentType = requireNativeView( + 'ExpoUI', + 'SlotView' +); + +function transformNavigationBarProps(props: NavigationBarProps): NavigationBarProps { + const { modifiers, ...restProps } = props; + return { + modifiers, + ...(modifiers ? createViewModifierEventListener(modifiers) : undefined), + ...restProps, + }; +} + +function transformNavigationBarItemProps( + props: NavigationBarItemProps +): NativeNavigationBarItemProps { + const { modifiers, onClick, ...restProps } = props; + return { + modifiers, + ...(modifiers ? createViewModifierEventListener(modifiers) : undefined), + ...restProps, + onButtonPressed: () => onClick?.(), + }; +} + +/** + * Icon slot for `NavigationBarItem`. + */ +function NavigationBarItemIcon(props: SlotProps) { + return {props.children}; +} + +/** + * Selected icon slot for `NavigationBarItem`. Falls back to `Icon` when omitted. + */ +function NavigationBarItemSelectedIcon(props: SlotProps) { + return {props.children}; +} + +/** + * Label slot for `NavigationBarItem`. + */ +function NavigationBarItemLabel(props: SlotProps) { + return {props.children}; +} + +/** + * A Material Design 3 navigation bar. + */ +export function NavigationBar(props: NavigationBarProps) { + const { children, ...restProps } = props; + return ( + + {children} + + ); +} + +/** + * A Material Design 3 navigation bar item. Must be used inside `NavigationBar`. + */ +function NavigationBarItemComponent(props: NavigationBarItemProps) { + const { children, ...restProps } = props; + return ( + + {children} + + ); +} + +NavigationBarItemComponent.Icon = NavigationBarItemIcon; +NavigationBarItemComponent.SelectedIcon = NavigationBarItemSelectedIcon; +NavigationBarItemComponent.Label = NavigationBarItemLabel; + +export { NavigationBarItemComponent as NavigationBarItem }; diff --git a/packages/expo-ui/src/jetpack-compose/index.ts b/packages/expo-ui/src/jetpack-compose/index.ts index a266cab370381c..5aedc12b0e97e1 100644 --- a/packages/expo-ui/src/jetpack-compose/index.ts +++ b/packages/expo-ui/src/jetpack-compose/index.ts @@ -41,6 +41,7 @@ export { export * from './ToggleButton'; export * from './Shape'; export * from './ModalBottomSheet'; +export * from './NavigationBar'; export * from './Carousel'; export { HorizontalPager, diff --git a/tools/src/commands/GenerateDocsAPIData.ts b/tools/src/commands/GenerateDocsAPIData.ts index a3881a444e45c9..7f526272426876 100644 --- a/tools/src/commands/GenerateDocsAPIData.ts +++ b/tools/src/commands/GenerateDocsAPIData.ts @@ -131,6 +131,7 @@ const uiPackagesMapping: Record = { 'expo-ui/jetpack-compose/progress': ['jetpack-compose/Progress/index.tsx', 'expo-ui'], 'expo-ui/jetpack-compose/listitem': ['jetpack-compose/ListItem/index.tsx', 'expo-ui'], 'expo-ui/jetpack-compose/modifiers': ['jetpack-compose/modifiers/index.ts', 'expo-ui'], + 'expo-ui/jetpack-compose/navigationbar': ['jetpack-compose/NavigationBar/index.tsx', 'expo-ui'], 'expo-ui/jetpack-compose/segmentedbutton': [ 'jetpack-compose/SegmentedButton/index.tsx', 'expo-ui', From d1f582162340fa891f420f12e39a72752e63bd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20=C5=9Awierk?= <58403334+sswrk@users.noreply.github.com> Date: Wed, 27 May 2026 09:39:32 +0200 Subject: [PATCH 2/7] [docs] EAS CLI build `--refresh-ad-hoc-provisioning-profile` flag documentation (#46290) # Why EAS CLI 19.1.0 adds `eas build --refresh-ad-hoc-provisioning-profile`, so non-interactive CI builds can refresh ad hoc provisioning profiles when an App Store Connect API key is set up. Our docs still say you must run `eas build` interactively after registering a device; this PR documents the new workflow. # How - Document the flag, prerequisites, and an example in [internal distribution](https://docs.expo.dev/build/internal-distribution/) (Automation on CI). - Cross-link from [building on CI](https://docs.expo.dev/build/building-on-ci/). # Test Plan Verified it renders correctly: Screenshot 2026-05-26 at 15 05 47 # 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) --- docs/pages/build/building-on-ci.mdx | 2 ++ docs/pages/build/internal-distribution.mdx | 24 +++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/pages/build/building-on-ci.mdx b/docs/pages/build/building-on-ci.mdx index 25bc4688a07327..554e66f0b4185f 100644 --- a/docs/pages/build/building-on-ci.mdx +++ b/docs/pages/build/building-on-ci.mdx @@ -96,6 +96,8 @@ Using the information you've gathered, pass it into the build command through en - `EXPO_APPLE_TEAM_ID`: Your Apple Team ID. For example, `77KQ969CHE`. - `EXPO_APPLE_TEAM_TYPE`: Your Apple Team Type. Valid types are `IN_HOUSE`, `COMPANY_OR_ORGANIZATION`, or `INDIVIDUAL`. +If you run [internal distribution](/build/internal-distribution/) builds on CI with ad hoc provisioning, refresh the ad hoc provisioning profile so registered devices added after the last build are included. For `eas build`, pass [`--refresh-ad-hoc-provisioning-profile`](/eas/cli/#eas-build) with `--non-interactive`. For [EAS Workflows](/eas/workflows/get-started), set `refresh_ad_hoc_provisioning_profile: true` in the build job's `params` ([build job parameters](/eas/workflows/pre-packaged-jobs#build)). See [Automation on CI](/build/internal-distribution/#automation-on-ci-optional) for requirements and an example command. + ### Trigger new builds Now that we're authenticated with Expo CLI, we can create the build step. diff --git a/docs/pages/build/internal-distribution.mdx b/docs/pages/build/internal-distribution.mdx index ed9315542d6052..8e08a7a378ecd4 100644 --- a/docs/pages/build/internal-distribution.mdx +++ b/docs/pages/build/internal-distribution.mdx @@ -30,7 +30,29 @@ See the tutorial on Internal distribution with EAS Build below for more informat ### Automation on CI (optional) -It's possible to run internal distribution builds non-interactively in CI using the `--non-interactive` flag. However, if you are using ad hoc provisioning on iOS you will not be able to add new devices to your provisioning profile when using this flag. After registering a device through `eas device:create`, you need to run `eas build` interactively and authenticate with Apple in order for EAS to add the device to your provisioning profile. [Learn more about triggering builds from CI](/build/building-on-ci). +You can run internal distribution builds non-interactively in CI with the [`--non-interactive`](/eas/cli/#eas-build) flag. [Learn more about triggering builds from CI](/build/building-on-ci). + +For iOS ad hoc builds, `eas build --non-interactive` reuses a valid provisioning profile without updating its device list. The build can succeed, but the app may not install on registered devices added after the profile was last updated. + +Pass `--refresh-ad-hoc-provisioning-profile` with `--non-interactive` to update the Expo-managed ad hoc provisioning profile on the Apple Developer Portal before the build. + +> **info** `--refresh-ad-hoc-provisioning-profile` requires EAS CLI 19.1.0 or later. + +EAS authenticates with an App Store Connect API key. It reads devices registered on EAS for your Apple team. It registers any missing UDIDs on the portal. Then it refreshes the profile device list. + +When you use this flag, EAS selects all matching devices for the build target's Apple platform: iPhone and iPad for iOS, Mac for macOS. + + + +For [EAS Workflows](/eas/workflows/get-started), set `refresh_ad_hoc_provisioning_profile: true` in the build job's `params` with the same profile requirements. See the [build job parameters](/eas/workflows/pre-packaged-jobs#build). + +The profile must set [`"distribution": "internal"`](/eas/json/#distribution) and use [credentials managed by EAS](/app-signing/app-credentials/). You need at least one device from [`eas device:create`](/eas/cli/#eas-devicecreate) and an App Store Connect API key in CI through [environment variables](/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team) (`EXPO_ASC_API_KEY_PATH`, `EXPO_ASC_KEY_ID`, and `EXPO_ASC_ISSUER_ID`) or a key stored in EAS for submissions on the project. + +Otherwise, after registering a device with [`eas device:create`](/eas/cli/#eas-devicecreate), run [`eas build`](/eas/cli/#eas-build) interactively and sign in with your Apple account so EAS can update the ad hoc provisioning profile. ### Managing devices From 32e32baceec55080194aedfb0a50c1a725bcdd4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20=C5=9Awierk?= <58403334+sswrk@users.noreply.github.com> Date: Wed, 27 May 2026 09:42:22 +0200 Subject: [PATCH 3/7] [docs] EAS workflows build job `refresh_ad_hoc_provisioning_profile` parameter documentation (#46291) # Why The base branch documents `eas build --refresh-ad-hoc-provisioning-profile` for internal iOS ad hoc builds on CI and links to EAS Workflows, but the workflows docs did not yet document `refresh_ad_hoc_provisioning_profile` on the build job. # How - Document `refresh_ad_hoc_provisioning_profile` on the `build job in pre-packaged-jobs.mdx` and `syntax.mdx` (syntax, parameters table, CLI flag mapping). - Add an example workflow for an internal iOS build with profile refresh. # Test Plan Verified it renders correctly: image image # 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) --- .../pages/eas/workflows/pre-packaged-jobs.mdx | 35 ++++++++++++++++--- docs/pages/eas/workflows/syntax.mdx | 1 + 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/pages/eas/workflows/pre-packaged-jobs.mdx b/docs/pages/eas/workflows/pre-packaged-jobs.mdx index 5c3c50bac0620c..ebe138c537c4d3 100644 --- a/docs/pages/eas/workflows/pre-packaged-jobs.mdx +++ b/docs/pages/eas/workflows/pre-packaged-jobs.mdx @@ -34,17 +34,19 @@ jobs: platform: android | ios # required profile: string # optional - default: production message: string # optional + refresh_ad_hoc_provisioning_profile: boolean # optional ``` #### Parameters You can pass the following parameters into the `params` list: -| Parameter | Type | Description | -| --------- | ------ | ------------------------------------------------------------------------------------------------------------- | -| platform | string | **Required.** The platform to build for. Can be either `android` or `ios`. | -| profile | string | Optional. The build profile to use. Defaults to `production`. | -| message | string | Optional. Custom message attached to the build. Corresponds to the `--message` flag when running `eas build`. | +| Parameter | Type | Description | +| ----------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| platform | string | **Required.** The platform to build for. Can be either `android` or `ios`. | +| profile | string | Optional. The build profile to use. Defaults to `production`. | +| message | string | Optional. Custom message attached to the build. Corresponds to the `--message` flag when running `eas build`. | +| refresh_ad_hoc_provisioning_profile | boolean | Optional. Refreshes the managed ad hoc provisioning profile before an iOS internal build starts. Corresponds to the [`--refresh-ad-hoc-provisioning-profile`](/eas/cli/#eas-build) flag when running `eas build`. See [internal distribution on CI](/build/internal-distribution/#automation-on-ci-optional). | #### Environment variables @@ -179,6 +181,29 @@ jobs: + + +This workflow builds your iOS app for internal distribution and refreshes the ad hoc provisioning profile when you push to the main branch. + +```yaml .eas/workflows/build-ios-internal.yml +name: Build iOS internal + +on: + push: + branches: ['main'] + +jobs: + build_ios: + name: Build iOS Internal + type: build + params: + platform: ios + profile: preview + refresh_ad_hoc_provisioning_profile: true +``` + + + ## Deploy Deploy your application using [EAS Hosting](/eas/hosting/introduction). diff --git a/docs/pages/eas/workflows/syntax.mdx b/docs/pages/eas/workflows/syntax.mdx index b75fbb69c76dcf..09f9c1ad7b7a6f 100644 --- a/docs/pages/eas/workflows/syntax.mdx +++ b/docs/pages/eas/workflows/syntax.mdx @@ -1038,6 +1038,7 @@ jobs: platform: ios | android # required profile: string # optional, default: production message: string # optional + refresh_ad_hoc_provisioning_profile: boolean # optional ``` This job outputs the following properties: From 8d41c4d706eea97b4cd63e5b2b55a8366ca45f14 Mon Sep 17 00:00:00 2001 From: Self Not Found Date: Wed, 27 May 2026 07:49:20 +0000 Subject: [PATCH 4/7] [file-system] Make File.write() async (#45992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Why Inspired by #44359. The current implementation of `File.write()` could block JS thread when writing a lot of data. The async `File.write()` could make the app more responsive. Plus, the legacy expo FileSystem API `writeAsStringAsync()` and the `write()` in `react-native-fs` are also async. I think the async API will help users migrating to the new `expo-file-system` # How 1. Extract common private function `writeToFile()` for the write logic for the iOS/Android native code 2. Wrap it with `AsyncFunction("write")`/`Function("writeSync")` 3. Update type definitions and comments in `expo-file-system` package 4. Replace all the old sync `File.write()` with `File.writeSync()`in tests 5. Add test cases for the new async `File.write()`, with/without different optiions 6. Update downstream calls of `File.write()` in this repo 7. Update docs This PR is largely based on the changes in #44359, as this is my first contribution to `expo` and I need reference implementations. # Test Plan 1. `CI=1 pnpm build`, `pnpm lint --fix` and `CI=1 pnpm test` passed in `packages/expo-file-system`
Output ``` > expo-module test PASS Android src/__tests__/FSNetworkTasks-test.native.ts PASS Android src/__tests__/FileSystem-test.native.ts PASS Node src/__tests__/FileSystemWatcher-test.ts PASS Web src/__tests__/FileSystemWatcher-test.ts PASS Android src/__tests__/FileSystemWatcher-test.ts PASS Android src/legacy/__tests__/FileSystem-test.native.ts PASS iOS src/__tests__/FileSystem-test.native.ts PASS iOS src/__tests__/FSNetworkTasks-test.native.ts PASS iOS src/__tests__/FileSystemWatcher-test.ts PASS iOS src/legacy/__tests__/FileSystem-test.native.ts Test Suites: 10 passed, 10 total Tests: 14 skipped, 204 passed, 218 total Snapshots: 0 total Time: 1.713 s Ran all test suites in 4 projects. ```
2. `pnpm test:ios` passed in `apps/bare-expo`
Screenshot image
3. `pnpm et check expo-file-system` passed in project root (tested on e81ba4c0548e008601b36aa8385908a83363d5e8)
Output ``` 🛠 Rebuilding expotools ✨ Successfully built expotools 🔍 Checking expo-file-system package 🏃‍♀️ Running pnpm run clean 🏃‍♀️ Running pnpm run build 🏃‍♀️ Running pnpm run test --watch false --passWithNoTests 🏃‍♀️ Running pnpm run lint --max-warnings 0 ✨ expo-file-system checks passed 🔌 Checking expo-file-system plugin 🏃‍♀️ Running pnpm run clean plugin 🏃‍♀️ Running pnpm run build plugin 🏃‍♀️ Running pnpm run test plugin --watch false --passWithNoTests 🏃‍♀️ Running pnpm run lint plugin --max-warnings 0 ✨ expo-file-system checks passed 🏁 All packages passed. ```
# Checklist - [x] 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) --- .../src/screens/FileSystemScreen.tsx | 14 +- apps/test-suite/tests/ContactsNext.ts | 4 +- apps/test-suite/tests/FileSystem.ts | 259 ++++++++++-------- .../pages/versions/unversioned/sdk/crypto.mdx | 2 +- .../versions/unversioned/sdk/filesystem.mdx | 8 +- packages/expo-file-system/CHANGELOG.md | 2 + .../modules/filesystem/FileSystemModule.kt | 43 +-- .../internal/NativeFileSystem.types.d.ts | 7 +- .../internal/NativeFileSystem.types.d.ts.map | 2 +- .../build/legacyWarnings.d.ts | 2 +- .../ios/FileSystemModule.swift | 41 ++- packages/expo-file-system/mocks/FileSystem.ts | 9 +- .../src/__tests__/FileSystem-test.native.ts | 71 +++-- .../src/internal/NativeFileSystem.types.ts | 8 +- .../expo-file-system/src/legacyWarnings.ts | 2 +- 15 files changed, 288 insertions(+), 186 deletions(-) diff --git a/apps/native-component-list/src/screens/FileSystemScreen.tsx b/apps/native-component-list/src/screens/FileSystemScreen.tsx index 1ebbb2f2b83fac..128a460f53388d 100644 --- a/apps/native-component-list/src/screens/FileSystemScreen.tsx +++ b/apps/native-component-list/src/screens/FileSystemScreen.tsx @@ -109,10 +109,10 @@ function FileSourcesSection({ setCurrentFile }: { setCurrentFile: (f: File) => v { + onPress={async () => { const file = new File(Paths.cache, 'test_sandbox', 'test.txt'); file.create({ intermediates: true, overwrite: true }); - file.write('Hello from FileSystem sandbox! Timestamp: ' + Date.now()); + await file.write('Hello from FileSystem sandbox! Timestamp: ' + Date.now()); setCurrentFile(file); Alert.alert('Created', file.uri); }} @@ -279,7 +279,7 @@ function ReadWriteSection({ withCurrentFile }: { withCurrentFile: WithCurrentFil { - file.write('Written at ' + new Date().toISOString()); + await file.write('Written at ' + new Date().toISOString()); return 'OK - size is now: ' + file.size; })} /> @@ -287,7 +287,7 @@ function ReadWriteSection({ withCurrentFile }: { withCurrentFile: WithCurrentFil title="write() base64" action={withCurrentFile(async (file) => { // Base64 of "Hello Base64!" - file.write('SGVsbG8gQmFzZTY0IQ==', { encoding: 'base64' }); + await file.write('SGVsbG8gQmFzZTY0IQ==', { encoding: 'base64' }); return 'OK - text() = ' + truncate(await file.text()); })} /> @@ -295,7 +295,7 @@ function ReadWriteSection({ withCurrentFile }: { withCurrentFile: WithCurrentFil title="write() Uint8Array" action={withCurrentFile(async (file) => { const bytes = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" - file.write(bytes); + await file.write(bytes); return 'OK - text() = ' + file.textSync(); })} /> @@ -687,7 +687,7 @@ function DirectoryOperationsSection({ title="Create file 'test_created.txt' in picked dir" action={async () => { const file = safDirectory.createFile('test_created.txt', 'text/plain'); - file.write('Created at ' + new Date().toISOString()); + await file.write('Created at ' + new Date().toISOString()); setCurrentFile(file); return { uri: file.uri, name: file.name }; }} @@ -788,7 +788,7 @@ function FileLifecycleSection({ const name = `test_${Date.now()}.txt`; const file = new File(Paths.cache, 'test_sandbox', name); file.create({ intermediates: true }); - file.write('Created for lifecycle test'); + await file.write('Created for lifecycle test'); setCurrentFile(file); return { uri: file.uri, exists: file.exists, size: file.size }; }} diff --git a/apps/test-suite/tests/ContactsNext.ts b/apps/test-suite/tests/ContactsNext.ts index 4a90c4a24a5352..1ba7e7b02cad71 100644 --- a/apps/test-suite/tests/ContactsNext.ts +++ b/apps/test-suite/tests/ContactsNext.ts @@ -120,7 +120,7 @@ export async function test(t) { const url = 'https://picsum.photos/200'; const response = await fetch(url); const src = new File(Paths.cache, 'file.pdf'); - src.write(await response.bytes()); + await src.write(await response.bytes()); const contactDetails = { givenName: 'Image', familyName: 'User', @@ -960,7 +960,7 @@ export async function test(t) { const url = 'https://picsum.photos/200'; const response = await fetch(url); const src = new File(Paths.cache, 'file.pdf'); - src.write(await response.bytes()); + await src.write(await response.bytes()); await contact.setImage(src.uri); const retrievedImage = await contact.getImage(); const retrievedThumbnail = await contact.getThumbnail(); diff --git a/apps/test-suite/tests/FileSystem.ts b/apps/test-suite/tests/FileSystem.ts index 9c3eb73dc2c166..6b7ced025bafe8 100644 --- a/apps/test-suite/tests/FileSystem.ts +++ b/apps/test-suite/tests/FileSystem.ts @@ -79,13 +79,13 @@ export async function test({ describe, expect, it, ...t }) { expect(safDirectory.list().length).toBe(0); const file = safDirectory.createFile('newFile', 'text/plain'); - file.write('test'); + file.writeSync('test'); expect(file.textSync()).toBe('test'); expect(file.bytesSync()).toEqual(new Uint8Array([116, 101, 115, 116])); expect(file.base64Sync()).toBe('dGVzdA=='); const file2 = safDirectory.createFile('newFile2', 'text/plain'); - file2.write(new Uint8Array([116, 101, 115, 116])); + file2.writeSync(new Uint8Array([116, 101, 115, 116])); expect(file2.textSync()).toBe('test'); expect(file2.size).toBe(4); expect(safDirectory.size).toBe(8); @@ -111,7 +111,7 @@ export async function test({ describe, expect, it, ...t }) { if (Platform.OS === 'ios') { it('allows picking files', async () => { const file = new File(testDirectory, 'selectMe.txt'); - file.write('test'); + file.writeSync('test'); const result = await File.pickFileAsync(testDirectory); const safFile = Array.isArray(result) ? result[0] : result; @@ -124,7 +124,7 @@ export async function test({ describe, expect, it, ...t }) { expect(directory.exists).toBe(true); const file = new File(directory, 'test.txt'); - file.write('test'); + file.writeSync('test'); expect(file.exists).toBe(true); const selectedDirectory = await Directory.pickDirectoryAsync(testDirectory); @@ -138,7 +138,7 @@ export async function test({ describe, expect, it, ...t }) { // Create a file in the selected directory const file2 = new File(directory.uri, 'newFile.txt'); - file2.write('test'); + file2.writeSync('test'); expect(file2.exists).toBe(true); expect(file2.textSync()).toBe('test'); @@ -249,13 +249,13 @@ export async function test({ describe, expect, it, ...t }) { it('emits a modified event when a watched file is written', async () => { const file = new File(watcherDirectory, 'modified.txt'); file.create(); - file.write('before'); + file.writeSync('before'); const events: { type: string }[] = []; const subscription = file.watch((event) => events.push(event)); try { - file.write('after'); + file.writeSync('after'); await delay(300); expect(events.some((event) => event.type === 'modified')).toBe(true); @@ -284,7 +284,7 @@ export async function test({ describe, expect, it, ...t }) { it('filters events by requested types', async () => { const file = new File(watcherDirectory, 'filter.txt'); file.create(); - file.write('before'); + file.writeSync('before'); const events: { type: string }[] = []; const subscription = file.watch((event) => events.push(event), { @@ -292,7 +292,7 @@ export async function test({ describe, expect, it, ...t }) { }); try { - file.write('after'); + file.writeSync('after'); await delay(300); expect(events.length).toBe(0); @@ -421,7 +421,7 @@ export async function test({ describe, expect, it, ...t }) { it('Works with spaces as filename', () => { const outputFile = new File(testDirectory, 'my new file.txt'); expect(outputFile.exists).toBe(false); - outputFile.write('Hello world'); + outputFile.writeSync('Hello world'); expect(outputFile.exists).toBe(true); expect(outputFile.name).toBe('my new file.txt'); }); @@ -487,37 +487,44 @@ export async function test({ describe, expect, it, ...t }) { it('Writes a string to a file reference', () => { const outputFile = new File(testDirectory, 'file.txt'); expect(outputFile.exists).toBe(false); - outputFile.write('Hello world'); + outputFile.writeSync('Hello world'); expect(outputFile.exists).toBe(true); }); it('overwrites files by default when using write()', () => { const file = new File(testDirectory, 'overwrite-test.txt'); - file.write('First'); + file.writeSync('First'); expect(file.textSync()).toBe('First'); - file.write('Second'); + file.writeSync('Second'); expect(file.textSync()).toBe('Second'); }); it('overwrites a longer file with a shorter string', () => { const file = new File(testDirectory, 'overwrite-shorter.txt'); - file.write('This is a long string'); + file.writeSync('This is a long string'); expect(file.textSync()).toBe('This is a long string'); - file.write('Short'); + file.writeSync('Short'); expect(file.textSync()).toBe('Short'); }); - it('Writes a base64 encoded string to a file reference', () => { + it('Writes a base64 encoded string to a file reference using File.writeSync', () => { const outputFile = new File(testDirectory, 'file.txt'); expect(outputFile.exists).toBe(false); - outputFile.write('SGVsbG8gd29ybGQh', { encoding: 'base64' }); + outputFile.writeSync('SGVsbG8gd29ybGQh', { encoding: 'base64' }); expect(outputFile.textSync()).toEqual('Hello world!'); }); - it('Writes a string to a file reference', async () => { + it('Writes a base64 encoded string to a file reference using File.write', async () => { + const outputFile = new File(testDirectory, 'file_async.txt'); + expect(outputFile.exists).toBe(false); + await outputFile.write('SGVsbG8gd29ybGQh', { encoding: 'base64' }); + expect(outputFile.textSync()).toEqual('Hello world!'); + }); + + it('Writes a string to a file reference using File.writeSync', async () => { const outputFile = new File(testDirectory, 'file.txt'); outputFile.create(); - outputFile.write(new Uint8Array([97, 98, 99])); + outputFile.writeSync(new Uint8Array([97, 98, 99])); expect(outputFile.exists).toBe(true); expect(await outputFile.bytes()).toEqual(new Uint8Array([97, 98, 99])); expect(outputFile.bytesSync()).toEqual(new Uint8Array([97, 98, 99])); @@ -525,9 +532,17 @@ export async function test({ describe, expect, it, ...t }) { expect(outputFile.textSync()).toBe('abc'); }); + it('Writes a string to a file reference using File.write', async () => { + const outputFile = new File(testDirectory, 'file_async.txt'); + outputFile.create(); + await outputFile.write('0abcd'); + expect(outputFile.exists).toBe(true); + expect(await outputFile.text()).toBe('0abcd'); + }); + it('Reads a string from a file reference', async () => { const outputFile = new File(testDirectory, 'file2.txt'); - outputFile.write('Hello world'); + outputFile.writeSync('Hello world'); expect(outputFile.exists).toBe(true); const content = await outputFile.text(); expect(content).toBe('Hello world'); @@ -564,30 +579,44 @@ export async function test({ describe, expect, it, ...t }) { expect(content).toBe('Hello world'); }); - it('appends a string using File.write', async () => { + it('appends a string using File.writeSync', async () => { const file = new File(testDirectory, 'next-append.txt'); - file.write('Hello'); - file.write(' world', { append: true }); + file.writeSync('Hello'); + file.writeSync(' world', { append: true }); expect(file.textSync()).toBe('Hello world'); }); - it('appends bytes using File.write', async () => { + it('appends a string using File.write', async () => { + const file = new File(testDirectory, 'next-append_async.txt'); + await file.write('Hello'); + await file.write(' world', { append: true }); + expect(file.textSync()).toBe('Hello world'); + }); + + it('appends bytes using File.writeSync', async () => { const file = new File(testDirectory, 'next-append-bytes.txt'); - file.write('Hello'); - file.write(new Uint8Array([32, 119, 111, 114, 108, 100]), { append: true }); // ' world' + file.writeSync('Hello'); + file.writeSync(new Uint8Array([32, 119, 111, 114, 108, 100]), { append: true }); // ' world' + expect(file.textSync()).toBe('Hello world'); + }); + + it('appends bytes using File.write', async () => { + const file = new File(testDirectory, 'next-append-bytes_async.txt'); + await file.write('Hello'); + await file.write(new Uint8Array([32, 119, 111, 114, 108, 100]), { append: true }); // ' world' expect(file.textSync()).toBe('Hello world'); }); it('creates a new file if append is true but file does not exist', async () => { const file = new File(testDirectory, 'new-file-append.txt'); - file.write('Hello', { append: true }); + file.writeSync('Hello', { append: true }); expect(file.textSync()).toBe('Hello'); }); }); it('Deletes a file reference', () => { const outputFile = new File(testDirectory, 'file3.txt'); - outputFile.write('Hello world'); + outputFile.writeSync('Hello world'); expect(outputFile.exists).toBe(true); outputFile.delete(); @@ -601,7 +630,7 @@ export async function test({ describe, expect, it, ...t }) { const childDir = new Directory(parentDir, 'child'); childDir.create(); const file = new File(childDir, 'file.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); expect(parentDir.exists).toBe(true); parentDir.delete(); expect(parentDir.exists).toBe(false); @@ -694,7 +723,7 @@ export async function test({ describe, expect, it, ...t }) { const file = new File(testDirectory, 'newFolder'); file.create(); expect(file.exists).toBe(true); - file.write('Hello world'); + file.writeSync('Hello world'); expect(file.textSync()).toBe('Hello world'); file.create({ overwrite: true }); expect(file.textSync()).toBe(''); @@ -732,7 +761,7 @@ export async function test({ describe, expect, it, ...t }) { it('Copies it to a folder', () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); const dstFolder = new Directory(testDirectory, 'destination'); dstFolder.create(); src.copySync(dstFolder); @@ -745,14 +774,14 @@ export async function test({ describe, expect, it, ...t }) { it('Throws an error when copying to a nonexistant folder without options', () => { const file = new File(testDirectory, 'file.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); const folder = new Directory(testDirectory, 'destination'); expect(() => file.copySync(folder)).toThrow(); }); it('Copies it to a file', () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); const dst = new File(testDirectory, 'file2.txt'); src.copySync(dst); expect(dst.exists).toBe(true); @@ -770,7 +799,7 @@ export async function test({ describe, expect, it, ...t }) { try { dst.delete(); } catch {} - src.write('Hello world'); + src.writeSync('Hello world'); src.copySync(dst); expect(dst.uri).toBe(FS.documentDirectory + 'file.txt'); expect(dst.exists).toBe(true); @@ -779,17 +808,17 @@ export async function test({ describe, expect, it, ...t }) { it('throws when destination file exists and overwrite is not set', () => { const src = new File(testDirectory, 'src.txt'); - src.write('source'); + src.writeSync('source'); const dst = new File(testDirectory, 'dst.txt'); - dst.write('destination'); + dst.writeSync('destination'); expect(() => src.copySync(dst)).toThrow(); }); it('overwrites destination file when overwrite is true', () => { const src = new File(testDirectory, 'src.txt'); - src.write('source'); + src.writeSync('source'); const dst = new File(testDirectory, 'dst.txt'); - dst.write('destination'); + dst.writeSync('destination'); src.copySync(dst, { overwrite: true }); expect(dst.textSync()).toBe('source'); expect(src.exists).toBe(true); @@ -797,11 +826,11 @@ export async function test({ describe, expect, it, ...t }) { it('overwrites file in destination directory when overwrite is true', () => { const src = new File(testDirectory, 'file.txt'); - src.write('new content'); + src.writeSync('new content'); const dstFolder = new Directory(testDirectory, 'destination'); dstFolder.create(); const existing = new File(dstFolder, 'file.txt'); - existing.write('old content'); + existing.writeSync('old content'); src.copySync(dstFolder, { overwrite: true }); expect(new File(dstFolder, 'file.txt').textSync()).toBe('new content'); }); @@ -846,11 +875,11 @@ export async function test({ describe, expect, it, ...t }) { it('overwrites destination directory when overwrite is true', () => { const src = new Directory(testDirectory, 'srcDir'); src.create(); - new File(src, 'file.txt').write('from source'); + new File(src, 'file.txt').writeSync('from source'); const dst = new Directory(testDirectory, 'dstDir'); dst.create(); - new File(dst, 'old.txt').write('old content'); + new File(dst, 'old.txt').writeSync('old content'); src.copySync(dst, { overwrite: true }); expect(dst.exists).toBe(true); @@ -867,7 +896,7 @@ export async function test({ describe, expect, it, ...t }) { it('moves it to a folder', () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); const dstFolder = new Directory(testDirectory, 'destination'); dstFolder.create(); src.moveSync(dstFolder); @@ -880,14 +909,14 @@ export async function test({ describe, expect, it, ...t }) { it('Throws an error when moving to a nonexistant folder without options', () => { const file = new File(testDirectory, 'file.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); const folder = new Directory(testDirectory, 'destination'); expect(() => file.moveSync(folder)).toThrow(); }); it('moves it to a file', () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); const dst = new File(testDirectory, 'file2.txt'); src.moveSync(dst); expect(dst.exists).toBe(true); @@ -898,17 +927,17 @@ export async function test({ describe, expect, it, ...t }) { it('throws when destination file exists and overwrite is not set', () => { const src = new File(testDirectory, 'src.txt'); - src.write('source'); + src.writeSync('source'); const dst = new File(testDirectory, 'dst.txt'); - dst.write('destination'); + dst.writeSync('destination'); expect(() => src.moveSync(dst)).toThrow(); }); it('overwrites destination file when overwrite is true', () => { const src = new File(testDirectory, 'src.txt'); - src.write('source'); + src.writeSync('source'); const dst = new File(testDirectory, 'dst.txt'); - dst.write('destination'); + dst.writeSync('destination'); src.moveSync(dst, { overwrite: true }); expect(dst.textSync()).toBe('source'); expect(src.uri).toBe(dst.uri); @@ -916,10 +945,10 @@ export async function test({ describe, expect, it, ...t }) { it('overwrites file in destination directory when overwrite is true', () => { const src = new File(testDirectory, 'file.txt'); - src.write('new content'); + src.writeSync('new content'); const dstFolder = new Directory(testDirectory, 'destination'); dstFolder.create(); - new File(dstFolder, 'file.txt').write('old content'); + new File(dstFolder, 'file.txt').writeSync('old content'); src.moveSync(dstFolder, { overwrite: true }); expect(new File(dstFolder, 'file.txt').textSync()).toBe('new content'); expect(src.uri).toBe(new File(dstFolder, 'file.txt').uri); @@ -929,7 +958,7 @@ export async function test({ describe, expect, it, ...t }) { describe('When renaming a file', () => { it('renames a file and updates its uri and existence', () => { const originalFile = new File(testDirectory, 'original.txt'); - originalFile.write('Hello world'); + originalFile.writeSync('Hello world'); originalFile.rename('renamed.txt'); expect(originalFile.exists).toBe(true); expect(originalFile.uri).toBe(testDirectory + 'renamed.txt'); @@ -937,7 +966,7 @@ export async function test({ describe, expect, it, ...t }) { it('renames a file and verifies it appears in the parent directory listing', () => { const fileToRename = new File(testDirectory, 'toRename.txt'); - fileToRename.write('Hello world'); + fileToRename.writeSync('Hello world'); fileToRename.rename('renamedFile.txt'); const parentDir = new Directory(testDirectory); @@ -948,7 +977,7 @@ export async function test({ describe, expect, it, ...t }) { it('ensures the old file name no longer exists after renaming', () => { const file = new File(testDirectory, 'oldName.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); file.rename('newName.txt'); expect(new File(testDirectory, 'oldName.txt').exists).toBe(false); expect(new File(testDirectory, 'newName.txt').exists).toBe(true); @@ -956,7 +985,7 @@ export async function test({ describe, expect, it, ...t }) { it('retains file contents after renaming', () => { const file = new File(testDirectory, 'contentFile.txt'); - file.write('Sample content'); + file.writeSync('Sample content'); file.rename('contentFileRenamed.txt'); const renamedFile = new File(testDirectory, 'contentFileRenamed.txt'); expect(renamedFile.textSync()).toBe('Sample content'); @@ -965,8 +994,8 @@ export async function test({ describe, expect, it, ...t }) { it('throws an error when renaming to an existing file name', () => { const file1 = new File(testDirectory, 'fileA.txt'); const file2 = new File(testDirectory, 'fileB.txt'); - file1.write('A'); - file2.write('B'); + file1.writeSync('A'); + file2.writeSync('B'); expect(() => file1.rename('fileB.txt')).toThrow(); }); @@ -977,7 +1006,7 @@ export async function test({ describe, expect, it, ...t }) { it('renames a file and preserves file metadata', () => { const file = new File(testDirectory, 'metadata.txt'); - file.write('Content'); + file.writeSync('Content'); const originalSize = file.size; const originalMd5 = file.md5; file.rename('metadataRenamed.txt'); @@ -987,15 +1016,15 @@ export async function test({ describe, expect, it, ...t }) { it('throws an error when renaming to an empty string', () => { const file = new File(testDirectory, 'file.txt'); - file.write('Content'); + file.writeSync('Content'); expect(() => file.rename('')).toThrow(); }); it('renames a file and updates parent directory listing correctly', () => { const file1 = new File(testDirectory, 'file1.txt'); const file2 = new File(testDirectory, 'file2.txt'); - file1.write('Content 1'); - file2.write('Content 2'); + file1.writeSync('Content 1'); + file2.writeSync('Content 2'); file1.rename('renamedFile1.txt'); @@ -1021,7 +1050,7 @@ export async function test({ describe, expect, it, ...t }) { it('Throws an error when moving to a nonexistant folder without options', () => { const file = new File(testDirectory, 'file.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); const folder = new Directory(testDirectory, 'some/nonexistent/directory/'); expect(() => file.moveSync(folder)).toThrow(); }); @@ -1046,13 +1075,13 @@ export async function test({ describe, expect, it, ...t }) { it('overwrites destination directory when overwrite is true', () => { const src = new Directory(testDirectory, 'srcDir'); src.create(); - new File(src, 'file.txt').write('from source'); + new File(src, 'file.txt').writeSync('from source'); const dstFolder = new Directory(testDirectory, 'destination'); dstFolder.create(); const dst = new Directory(dstFolder, 'srcDir'); dst.create(); - new File(dst, 'old.txt').write('old content'); + new File(dst, 'old.txt').writeSync('old content'); src.moveSync(dstFolder, { overwrite: true }); expect(src.exists).toBe(true); @@ -1109,7 +1138,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Copy operations - SAF file', () => { it('copies SAF file -> SAF directory (creates file inside)', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const dstDir = safDirectory.createDirectory('targetDir'); srcFile.copySync(dstDir); @@ -1123,7 +1152,7 @@ export async function test({ describe, expect, it, ...t }) { it('copies SAF file -> local file', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const dstFile = new File(localDirectory, 'dest.txt'); srcFile.copySync(dstFile); @@ -1135,7 +1164,7 @@ export async function test({ describe, expect, it, ...t }) { it('copies SAF file -> local directory (creates file inside)', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test content'); + srcFile.writeSync('test content'); srcFile.copySync(localDirectory); @@ -1149,7 +1178,7 @@ export async function test({ describe, expect, it, ...t }) { it('copies SAF directory -> SAF directory (existing)', () => { const srcDir = safDirectory.createDirectory('sourceDir'); const srcFile = srcDir.createFile('nested.txt', 'text/plain'); - srcFile.write('nested content'); + srcFile.writeSync('nested content'); const dstDir = safDirectory.createDirectory('destDir'); srcDir.copySync(dstDir); @@ -1166,7 +1195,7 @@ export async function test({ describe, expect, it, ...t }) { it('copies SAF directory -> local directory (existing)', () => { const srcDir = safDirectory.createDirectory('sourceDir2'); const srcFile = srcDir.createFile('nested.txt', 'text/plain'); - srcFile.write('nested content'); + srcFile.writeSync('nested content'); // Destination directory must exist for directory copy srcDir.copySync(localDirectory); @@ -1183,7 +1212,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Copy operations - local to SAF', () => { it('copies local file -> SAF directory (creates file inside)', () => { const srcFile = new File(localDirectory, 'localfile.txt'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const dstDir = safDirectory.createDirectory('localToSafDir'); srcFile.copySync(dstDir); @@ -1198,7 +1227,7 @@ export async function test({ describe, expect, it, ...t }) { const srcDir = new Directory(localDirectory, 'localSourceDir'); srcDir.create(); const srcFile = new File(srcDir, 'nested.txt'); - srcFile.write('nested content'); + srcFile.writeSync('nested content'); srcDir.copySync(safDirectory); @@ -1251,7 +1280,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Move operations - SAF file', () => { it('moves SAF file -> SAF directory (creates file inside)', () => { const srcFile = safDirectory.createFile('moveSource.txt', 'text/plain'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const originalUri = srcFile.uri; const dstDir = safDirectory.createDirectory('moveTargetDir'); @@ -1269,7 +1298,7 @@ export async function test({ describe, expect, it, ...t }) { it('moves SAF file -> local file', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const originalUri = srcFile.uri; const dstFile = new File(localDirectory, 'dest.txt'); @@ -1283,7 +1312,7 @@ export async function test({ describe, expect, it, ...t }) { it('moves SAF file -> local directory (creates file inside)', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const originalUri = srcFile.uri; srcFile.moveSync(localDirectory); @@ -1300,7 +1329,7 @@ export async function test({ describe, expect, it, ...t }) { it('moves SAF directory -> SAF directory (existing)', () => { const srcDir = safDirectory.createDirectory('moveSrcDir'); const srcFile = srcDir.createFile('nested.txt', 'text/plain'); - srcFile.write('nested content'); + srcFile.writeSync('nested content'); const originalUri = srcDir.uri; const dstDir = safDirectory.createDirectory('moveDestDir'); @@ -1316,7 +1345,7 @@ export async function test({ describe, expect, it, ...t }) { it('moves SAF directory -> local directory (existing)', () => { const srcDir = safDirectory.createDirectory('moveSrcDir2'); const srcFile = srcDir.createFile('nested.txt', 'text/plain'); - srcFile.write('nested content'); + srcFile.writeSync('nested content'); const originalUri = srcDir.uri; const localDest = new Directory(localDirectory, 'safMoveTarget'); @@ -1335,7 +1364,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Move operations - local to SAF', () => { it('moves local file -> SAF directory (creates file inside)', () => { const srcFile = new File(localDirectory, 'localMoveFile.txt'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const originalUri = srcFile.uri; const dstDir = safDirectory.createDirectory('localMoveTarget'); @@ -1357,7 +1386,7 @@ export async function test({ describe, expect, it, ...t }) { const srcDir = new Directory(localDirectory, 'localMoveDir'); srcDir.create(); const srcFile = new File(srcDir, 'nested.txt'); - srcFile.write('nested content'); + srcFile.writeSync('nested content'); const originalUri = srcDir.uri; srcDir.moveSync(safDirectory); @@ -1397,7 +1426,7 @@ export async function test({ describe, expect, it, ...t }) { it('throws when destination directory does not exist (file copy)', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test'); + srcFile.writeSync('test'); const nonExistentDir = new Directory(localDirectory, 'nonexistent'); expect(() => srcFile.copySync(nonExistentDir)).toThrow(); @@ -1405,7 +1434,7 @@ export async function test({ describe, expect, it, ...t }) { it('throws when destination directory does not exist (file move)', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test'); + srcFile.writeSync('test'); const nonExistentDir = new Directory(localDirectory, 'nonexistent'); expect(() => srcFile.moveSync(nonExistentDir)).toThrow(); @@ -1432,7 +1461,7 @@ export async function test({ describe, expect, it, ...t }) { const dir = new Directory(testDirectory, 'contentDir/'); dir.create(); const file = new File(dir, 'file.txt'); - file.write('test'); + file.writeSync('test'); dir.rename('renamedContentDir'); const renamedDir = new Directory(testDirectory, 'renamedContentDir/'); expect(renamedDir.exists).toBe(true); @@ -1470,7 +1499,7 @@ export async function test({ describe, expect, it, ...t }) { const dir = new Directory(testDirectory, 'metadataDir/'); dir.create(); const file = new File(dir, 'test.txt'); - file.write('Test content'); + file.writeSync('Test content'); const originalSize = dir.size; @@ -1567,7 +1596,7 @@ export async function test({ describe, expect, it, ...t }) { const md5 = '2942bfabb3d05332b66eb128e0842cff'; const response = await fetch(url); const src = new File(testDirectory, 'file.pdf'); - src.write(await response.bytes()); + src.writeSync(await response.bytes()); expect(src.md5).toEqual(md5); }); @@ -1740,7 +1769,7 @@ export async function test({ describe, expect, it, ...t }) { const url = 'https://httpbingo.org/bytes/10240'; const file = new File(testDirectory, 'idempotent_progress.bin'); file.create(); - file.write('existing content'); + file.writeSync('existing content'); const progressUpdates: { bytesWritten: number; totalBytes: number }[] = []; @@ -1761,7 +1790,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Computes file properties', () => { it('computes size', async () => { const file = new File(testDirectory, 'file.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); expect(file.size).toBe(11); }); @@ -1770,7 +1799,7 @@ export async function test({ describe, expect, it, ...t }) { testDirectory, 'creationTime_is_earlier_than_modificationTime_or_equal.txt' ); - file.write('Hello world'); + file.writeSync('Hello world'); expect(file.creationTime).not.toBeNull(); expect(file.modificationTime).not.toBeNull(); expect(file.creationTime).toBeLessThanOrEqual(file.modificationTime); @@ -1778,7 +1807,7 @@ export async function test({ describe, expect, it, ...t }) { it('computes md5', async () => { const file = new File(testDirectory, 'file.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); expect(file.md5).toBe('3e25960a79dbc69b674cd4ec67a72c62'); }); @@ -1794,7 +1823,7 @@ export async function test({ describe, expect, it, ...t }) { const dir = new Directory(testDirectory, 'directory'); const file = new File(testDirectory, 'directory', 'file.txt'); file.create({ intermediates: true }); - file.write('Hello world'); + file.writeSync('Hello world'); expect(dir.size).toBe(11); }); }); @@ -1802,7 +1831,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Returns base64', () => { it('gets base64 of a file', async () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); expect(await src.base64()).toBe('SGVsbG8gd29ybGQ='); expect(src.base64Sync()).toBe('SGVsbG8gd29ybGQ='); }); @@ -1811,7 +1840,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Returns bytes', () => { it('gets file as a Uint8Array', async () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); expect(src.bytesSync()).toEqual( new Uint8Array([72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]) ); @@ -1911,7 +1940,7 @@ export async function test({ describe, expect, it, ...t }) { const url = `${testDirectory}execute_correctly.txt`; const src = new File(url); src.create(); - src.write('Hello World'); + src.writeSync('Hello World'); const result = src.info({ md5: true }); expect(result.exists).toBe(true); if (result.exists) { @@ -1926,7 +1955,7 @@ export async function test({ describe, expect, it, ...t }) { it('executes correctly when options are undefined', () => { const url = `${testDirectory}executes_correctly_when_options_are_undefined.txt`; const src = new File(url); - src.write('Hello World'); + src.writeSync('Hello World'); const result = src.info(); if (result.exists) { expect(result.md5).toBeNull(); @@ -1935,7 +1964,7 @@ export async function test({ describe, expect, it, ...t }) { it('returns exists false if file does not exist', () => { const url = `${testDirectory}returns_exists_false_if_file_does_not_exist.txt`; const src = new File(url); - src.write('Hello world'); + src.writeSync('Hello world'); src.delete(); const result = src.info(); expect(result.exists).toBe(false); @@ -1960,7 +1989,7 @@ export async function test({ describe, expect, it, ...t }) { const src = new Directory(url); src.create(); const file = new File(`${url}1.txt`); - file.write('Hello world'); + file.writeSync('Hello world'); const result = src.info(); @@ -1997,7 +2026,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Exposes file handles', () => { it('Allows opening files', () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); const handle = src.open(); expect(handle.readBytes(4)).toEqual(new Uint8Array([72, 101, 108, 108])); // Hell expect(handle.readBytes(4)).toEqual(new Uint8Array([111, 32, 119, 111])); // o wo @@ -2009,7 +2038,7 @@ export async function test({ describe, expect, it, ...t }) { it('Resets position on close', () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); let handle = src.open(); expect(handle.readBytes(1)).toEqual(new Uint8Array([97])); // a handle.close(); @@ -2020,7 +2049,7 @@ export async function test({ describe, expect, it, ...t }) { it('Throws on reading from closed handle', () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const handle = src.open(); expect(handle.readBytes(1)).toEqual(new Uint8Array([97])); // a handle.close(); @@ -2029,7 +2058,7 @@ export async function test({ describe, expect, it, ...t }) { it('Can open multiple handles to the same file', () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const handle = src.open(); const handle2 = src.open(); expect(handle.readBytes(1)).toEqual(new Uint8Array([97])); // a @@ -2038,7 +2067,7 @@ export async function test({ describe, expect, it, ...t }) { it('Returns null offset on closed handle', () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const handle = src.open(); handle.close(); expect(handle.offset).toBe(null); @@ -2046,7 +2075,7 @@ export async function test({ describe, expect, it, ...t }) { it('Returns null size on closed handle', () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const handle = src.open(); handle.close(); expect(handle.size).toBe(null); @@ -2058,7 +2087,7 @@ export async function test({ describe, expect, it, ...t }) { const handle = src.open(); expect(handle.readBytes(2)).toEqual(new Uint8Array([])); // a handle.close(); - src.write('abcde'); + src.writeSync('abcde'); const handle2 = src.open(); expect(handle2.readBytes(1)).toEqual(new Uint8Array([97])); // a handle2.close(); @@ -2067,7 +2096,7 @@ export async function test({ describe, expect, it, ...t }) { it('Reads a file in chunks', () => { const src = new File(testDirectory, 'abcs.txt'); const alphabet = 'abcdefghijklmnopqrstuvwxyz'; - src.write(alphabet.repeat(1000) + 'ending'); + src.writeSync(alphabet.repeat(1000) + 'ending'); const handle = src.open(); for (let i = 0; i < 250; i++) { const chunk = handle.readBytes(26 * 4); @@ -2105,7 +2134,7 @@ export async function test({ describe, expect, it, ...t }) { describe('It supports different FileMode options', () => { it('opens in ReadOnly mode and reads data', () => { const src = new File(testDirectory, 'mode-read.txt'); - src.write('Hello'); + src.writeSync('Hello'); const handle = src.open(FileMode.ReadOnly); expect(handle.readBytes(5)).toEqual(new Uint8Array([72, 101, 108, 108, 111])); handle.close(); @@ -2113,7 +2142,7 @@ export async function test({ describe, expect, it, ...t }) { it('throws when writing to a ReadOnly handle', () => { const src = new File(testDirectory, 'mode-read-only.txt'); - src.write('Hello'); + src.writeSync('Hello'); const handle = src.open(FileMode.ReadOnly); expect(() => handle.writeBytes(new Uint8Array([65]))).toThrow(); handle.close(); @@ -2138,7 +2167,7 @@ export async function test({ describe, expect, it, ...t }) { it('opens in ReadWrite mode and supports both reading and writing', () => { const src = new File(testDirectory, 'mode-rw.txt'); - src.write('Hello'); + src.writeSync('Hello'); const handle = src.open(FileMode.ReadWrite); expect(handle.readBytes(5)).toEqual(new Uint8Array([72, 101, 108, 108, 111])); handle.offset = 0; @@ -2149,7 +2178,7 @@ export async function test({ describe, expect, it, ...t }) { it('opens in Append mode and appends data', () => { const src = new File(testDirectory, 'mode-append.txt'); - src.write('Hello'); + src.writeSync('Hello'); const handle = src.open(FileMode.Append); handle.writeBytes(new Uint8Array([32, 87, 111, 114, 108, 100])); // ' World' handle.close(); @@ -2158,7 +2187,7 @@ export async function test({ describe, expect, it, ...t }) { it('opens in Truncate mode and wipes existing content', () => { const src = new File(testDirectory, 'mode-truncate.txt'); - src.write('Old content'); + src.writeSync('Old content'); const handle = src.open(FileMode.Truncate); expect(handle.size).toBe(0); handle.writeBytes(new Uint8Array([78, 101, 119])); // New @@ -2170,7 +2199,7 @@ export async function test({ describe, expect, it, ...t }) { it('Provides a ReadableStream', async () => { const src = new File(testDirectory, 'abcs.txt'); const alphabet = 'abcdefghijklmnopqrstuvwxyz'; - src.write(alphabet); + src.writeSync(alphabet); const stream = src.readableStream(); for await (const chunk of stream) { expect(chunk[0]).toBe(alphabet.charCodeAt(0)); @@ -2180,7 +2209,7 @@ export async function test({ describe, expect, it, ...t }) { it('Provides a ReadableStream with byob support', async () => { const src = new File(testDirectory, 'abcs.txt'); const alphabet = 'abcdefghij'.repeat(1000); - src.write(alphabet); + src.writeSync(alphabet); const stream = src.readableStream(); const array1 = new Uint8Array(5000); const array2 = new Uint8Array(5000); @@ -2212,14 +2241,14 @@ export async function test({ describe, expect, it, ...t }) { const src = new File(asset.localUri); expect(src.type).toBe('image/jpeg'); const src2 = new File(testDirectory, 'file.txt'); - src2.write('abcde'); + src2.writeSync('abcde'); expect(src2.type).toBe('text/plain'); }); // You can also use something like container twostoryrobot/simple-file-upload to test if the file is saved correctly it('Supports sending a file using blob', async () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const response = await fetch('https://httpbingo.org/anything', { method: 'POST', @@ -2232,7 +2261,7 @@ export async function test({ describe, expect, it, ...t }) { // You can also use this docker image: twostoryrobot/simple-file-upload to test e2e blob upload. it('Supports sending a file using blob with formdata', async () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const formData = new FormData(); @@ -2248,7 +2277,7 @@ export async function test({ describe, expect, it, ...t }) { it('Supports sending a named file blob using blob with formdata', async () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const formData = new FormData(); @@ -2292,13 +2321,13 @@ function addAppleAppGroupsTestSuiteAsync({ describe, expect, it, ...t }) { scopedIt('Writes a string to a file reference', () => { const outputFile = new File(sharedContainerTestDir, 'file.txt'); expect(outputFile.exists).toBe(false); - outputFile.write('Hello world'); + outputFile.writeSync('Hello world'); expect(outputFile.exists).toBe(true); }); scopedIt('Deletes a file reference', () => { const outputFile = new File(sharedContainerTestDir, 'file3.txt'); - outputFile.write('Hello world'); + outputFile.writeSync('Hello world'); expect(outputFile.exists).toBe(true); outputFile.delete(); diff --git a/docs/pages/versions/unversioned/sdk/crypto.mdx b/docs/pages/versions/unversioned/sdk/crypto.mdx index 9ca6d0f3f6bafa..2ddfbc8e0f3565 100644 --- a/docs/pages/versions/unversioned/sdk/crypto.mdx +++ b/docs/pages/versions/unversioned/sdk/crypto.mdx @@ -125,7 +125,7 @@ async function encryptAndSaveData(plaintextData: Uint8Array) { // Save encrypted file const file = new File(Paths.cache, 'encrypted.dat'); file.create({ overwrite: true }); - file.write(encryptedBytes); + await file.write(encryptedBytes); } ``` diff --git a/docs/pages/versions/unversioned/sdk/filesystem.mdx b/docs/pages/versions/unversioned/sdk/filesystem.mdx index 2327fbc68eccfd..eaa5a52bcba55f 100644 --- a/docs/pages/versions/unversioned/sdk/filesystem.mdx +++ b/docs/pages/versions/unversioned/sdk/filesystem.mdx @@ -107,7 +107,7 @@ import { File, Paths } from 'expo-file-system'; try { const file = new File(Paths.cache, 'example.txt'); file.create(); // can throw an error if the file already exists or no permission to create it - file.write('Hello, world!'); + await file.write('Hello, world!'); // or `file.writeSync('Hello, world!');` for synchronous call console.log(file.textSync()); // Hello, world! } catch (error) { console.error(error); @@ -179,7 +179,7 @@ import { File, Paths } from 'expo-file-system'; const url = 'https://pdfobject.com/pdf/sample.pdf'; const response = await fetch(url); const src = new File(Paths.cache, 'file.pdf'); -src.write(await response.bytes()); +await src.write(await response.bytes()); ``` @@ -193,7 +193,7 @@ import { fetch } from 'expo/fetch'; import { File, Paths } from 'expo-file-system'; const file = new File(Paths.cache, 'file.txt'); -file.write('Hello, world!'); +await file.write('Hello, world!'); const response = await fetch('https://example.com', { method: 'POST', @@ -208,7 +208,7 @@ import { fetch } from 'expo/fetch'; import { File, Paths } from 'expo-file-system'; const file = new File(Paths.cache, 'file.txt'); -file.write('Hello, world!'); +await file.write('Hello, world!'); const formData = new FormData(); formData.append('data', file); const response = await fetch('https://example.com', { diff --git a/packages/expo-file-system/CHANGELOG.md b/packages/expo-file-system/CHANGELOG.md index 5b7d5c2be32484..7085ca9339b7b5 100644 --- a/packages/expo-file-system/CHANGELOG.md +++ b/packages/expo-file-system/CHANGELOG.md @@ -4,6 +4,8 @@ ### 🛠 Breaking changes +- `File.write()` is now asynchronous and returns a Promise. Use `File.writeSync()` for synchronous behavior. ([#45992](https://github.com/expo/expo/pull/45992) by [@wh201906](https://github.com/wh201906)) + ### 🎉 New features ### 🐛 Bug fixes diff --git a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt index 5ad4088a817c8a..8e73636f4475b8 100644 --- a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt +++ b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt @@ -29,6 +29,27 @@ class FileSystemModule : Module() { private val downloadStore = DownloadTaskStore() + private fun writeToFile( + file: FileSystemFile, + content: Either, + options: WriteOptions? + ) { + val append = options?.append ?: false + if (content.`is`(String::class)) { + content.get(String::class).let { + if (options?.encoding == EncodingType.BASE64) { + file.write(Base64.decode(it, Base64.DEFAULT), append) + } else { + file.write(it, append) + } + } + } else if (content.`is`(TypedArray::class)) { + content.get(TypedArray::class).let { + file.write(it, append) + } + } + } + @RequiresApi(Build.VERSION_CODES.O) override fun definition() = ModuleDefinition { Name("FileSystem") @@ -138,22 +159,12 @@ class FileSystemModule : Module() { file.create(options ?: CreateOptions()) } - Function("write") { file: FileSystemFile, content: Either, options: WriteOptions? -> - val append = options?.append ?: false - if (content.`is`(String::class)) { - content.get(String::class).let { - if (options?.encoding == EncodingType.BASE64) { - file.write(Base64.decode(it, Base64.DEFAULT), append) - } else { - file.write(it, append) - } - } - } - if (content.`is`(TypedArray::class)) { - content.get(TypedArray::class).let { - file.write(it, append) - } - } + AsyncFunction("write") Coroutine { file: FileSystemFile, content: Either, options: WriteOptions? -> + writeToFile(file, content, options) + } + + Function("writeSync") { file: FileSystemFile, content: Either, options: WriteOptions? -> + writeToFile(file, content, options) } AsyncFunction("text") { file: FileSystemFile -> diff --git a/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts b/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts index d3c5e0a093b677..930b1841721d0c 100644 --- a/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts +++ b/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts @@ -152,7 +152,12 @@ export declare class NativeFileSystemFile { * Writes content to the file. * @param content The content to write into the file. */ - write(content: string | Uint8Array, options?: FileWriteOptions): void; + write(content: string | Uint8Array, options?: FileWriteOptions): Promise; + /** + * Writes content to the file. + * @param content The content to write into the file. + */ + writeSync(content: string | Uint8Array, options?: FileWriteOptions): void; /** * Deletes a file. * diff --git a/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts.map b/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts.map index e630122e944ec5..bb23bffd09b80f 100644 --- a/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts.map +++ b/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"NativeFileSystem.types.d.ts","sourceRoot":"","sources":["../../src/internal/NativeFileSystem.types.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"NativeFileSystem.types.d.ts","sourceRoot":"","sources":["../../src/internal/NativeFileSystem.types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/expo-file-system/build/legacyWarnings.d.ts b/packages/expo-file-system/build/legacyWarnings.d.ts index 647c8d3c0c23ed..c6d045cebdc205 100644 --- a/packages/expo-file-system/build/legacyWarnings.d.ts +++ b/packages/expo-file-system/build/legacyWarnings.d.ts @@ -12,7 +12,7 @@ export declare function readAsStringAsync(fileUri: string, options?: ReadingOpti */ export declare function getContentUriAsync(fileUri: string): Promise; /** - * @deprecated Use `new File().write()` or import this method from `expo-file-system/legacy`. This method will throw in runtime. + * @deprecated Use `await new File().write()` or `new File().writeSync()` or import this method from `expo-file-system/legacy`. This method will throw in runtime. */ export declare function writeAsStringAsync(fileUri: string, contents: string, options?: WritingOptions): Promise; /** diff --git a/packages/expo-file-system/ios/FileSystemModule.swift b/packages/expo-file-system/ios/FileSystemModule.swift index 7af943c7823e2d..7d44c0a38493a4 100644 --- a/packages/expo-file-system/ios/FileSystemModule.swift +++ b/packages/expo-file-system/ios/FileSystemModule.swift @@ -34,6 +34,26 @@ public final class FileSystemModule: Module { return attributes[.systemFreeSize] as? Int64 } + private func writeToFile( + _ file: FileSystemFile, + content: Either, + options: WriteOptions? + ) throws { + let append = options?.append ?? false + if let content: String = content.get() { + if options?.encoding == WriteEncoding.base64 { + guard let data = Data(base64Encoded: content, options: .ignoreUnknownCharacters) else { + throw UnableToWriteBase64DataException(file.url.absoluteString) + } + try file.write(data, append: append) + } else { + try file.write(content, append: append) + } + } else if let content: TypedArray = content.get() { + try file.write(content, append: append) + } + } + public func definition() -> ModuleDefinition { Name("FileSystem") @@ -174,21 +194,12 @@ public final class FileSystemModule: Module { return try file.info(options: options ?? InfoOptions()) } - Function("write") { (file: FileSystemFile, content: Either, options: WriteOptions?) in - let append = options?.append ?? false - if let content: String = content.get() { - if options?.encoding == WriteEncoding.base64 { - guard let data = Data(base64Encoded: content, options: .ignoreUnknownCharacters) else { - throw UnableToWriteBase64DataException(file.url.absoluteString) - } - try file.write(data, append: append) - } else { - try file.write(content, append: append) - } - } - if let content: TypedArray = content.get() { - try file.write(content, append: append) - } + AsyncFunction("write") { (file: FileSystemFile, content: Either, options: WriteOptions?) in + try writeToFile(file, content: content, options: options) + } + + Function("writeSync") { (file: FileSystemFile, content: Either, options: WriteOptions?) in + try writeToFile(file, content: content, options: options) } Property("size") { file in diff --git a/packages/expo-file-system/mocks/FileSystem.ts b/packages/expo-file-system/mocks/FileSystem.ts index 53bb7ad621a12b..30f6cbde9aacec 100644 --- a/packages/expo-file-system/mocks/FileSystem.ts +++ b/packages/expo-file-system/mocks/FileSystem.ts @@ -246,7 +246,7 @@ export class FileSystemFile { }); } - write( + writeSync( content: string | Uint8Array, options: { append?: boolean; encoding?: 'utf8' | 'base64' } = {} ): void { @@ -280,6 +280,13 @@ export class FileSystemFile { } } + async write( + content: string | Uint8Array, + options: { append?: boolean; encoding?: 'utf8' | 'base64' } = {} + ): Promise { + this.writeSync(content, options); + } + private readBytesOrThrow(): Uint8Array { const entry = store.get(normalizeKey(this.uri)); if (!entry || entry.kind !== 'file' || !entry.exists) { diff --git a/packages/expo-file-system/src/__tests__/FileSystem-test.native.ts b/packages/expo-file-system/src/__tests__/FileSystem-test.native.ts index d51f7239ed6632..7b65243e446e40 100644 --- a/packages/expo-file-system/src/__tests__/FileSystem-test.native.ts +++ b/packages/expo-file-system/src/__tests__/FileSystem-test.native.ts @@ -62,6 +62,7 @@ describe('expo-file-system new API', () => { expect(typeof file.moveSync).toBe('function'); expect(typeof file.text).toBe('function'); expect(typeof file.write).toBe('function'); + expect(typeof file.writeSync).toBe('function'); expect(typeof file.json).toBe('function'); expect(typeof file.formData).toBe('function'); }); @@ -122,6 +123,15 @@ describe('expo-file-system new API', () => { await expect(file.move(destination, { overwrite: true })).resolves.toBeUndefined(); }); + it('File.write returns a promise and File.writeSync stays synchronous', async () => { + const file = new File(Paths.cache, 'test.txt'); + + expect(file.write('hello')).toBeInstanceOf(Promise); + const result = await file.write('hello'); + expect(result).toBeUndefined(); + expect(file.writeSync('hello')).toBeUndefined(); + }); + it('Directory copy and move return promises', async () => { const dir = new Directory(Paths.document, 'subdir'); dir.create(); @@ -193,8 +203,8 @@ describe('expo-file-system behavioral mock', () => { const dir = new Directory(Paths.cache, 'info-dir'); dir.create(); const creationTime = dir.info().creationTime; - dir.createFile('a.txt', null).write('abc'); - dir.createFile('b.txt', null).write('de'); + dir.createFile('a.txt', null).writeSync('abc'); + dir.createFile('b.txt', null).writeSync('de'); expect(dir.info()).toMatchObject({ exists: true, @@ -204,32 +214,53 @@ describe('expo-file-system behavioral mock', () => { }); }); - it('File.write(string) and File.text() roundtrip utf-8', async () => { + it('File.writeSync(string) and File.text() roundtrip utf-8', async () => { const file = new File(Paths.cache, 'hello.txt'); - file.write('hello world'); + file.writeSync('hello world'); + expect(file.textSync()).toBe('hello world'); + await expect(file.text()).resolves.toBe('hello world'); + }); + + it('File.write(string) and File.text() roundtrip utf-8', async () => { + const file = new File(Paths.cache, 'hello_async.txt'); + await file.write('hello world'); expect(file.textSync()).toBe('hello world'); await expect(file.text()).resolves.toBe('hello world'); }); - it('File.write(Uint8Array) and File.bytes() roundtrip byte-for-byte', async () => { + it('File.writeSync(Uint8Array) and File.bytes() roundtrip byte-for-byte', async () => { const file = new File(Paths.cache, 'bin.dat'); const payload = new Uint8Array([1, 2, 3, 4, 5]); - file.write(payload); + file.writeSync(payload); expect(Array.from(file.bytesSync())).toEqual([1, 2, 3, 4, 5]); await expect(file.bytes()).resolves.toEqual(payload); }); - it('File.write with append option appends to existing bytes', () => { + it('File.writeSync with append option appends to existing bytes', () => { const file = new File(Paths.cache, 'log.txt'); - file.write('a'); - file.write('b', { append: true }); - file.write('c', { append: true }); + file.writeSync('a'); + file.writeSync('b', { append: true }); + file.writeSync('c', { append: true }); + expect(file.textSync()).toBe('abc'); + }); + + it('File.write with append option appends to existing bytes', async () => { + const file = new File(Paths.cache, 'log_async.txt'); + await file.write('a'); + await file.write('b', { append: true }); + await file.write('c', { append: true }); expect(file.textSync()).toBe('abc'); }); - it('File.write with base64 encoding decodes before storing', () => { + it('File.writeSync with base64 encoding decodes before storing', () => { const file = new File(Paths.cache, 'encoded.txt'); - file.write(Buffer.from('hello').toString('base64'), { encoding: 'base64' }); + file.writeSync(Buffer.from('hello').toString('base64'), { encoding: 'base64' }); + expect(file.textSync()).toBe('hello'); + }); + + it('File.write with base64 encoding decodes before storing', async () => { + const file = new File(Paths.cache, 'encoded_async.txt'); + await file.write(Buffer.from('hello').toString('base64'), { encoding: 'base64' }); expect(file.textSync()).toBe('hello'); }); @@ -247,7 +278,7 @@ describe('expo-file-system behavioral mock', () => { expect(creationTime).toBeGreaterThan(0); expect(file.modificationTime).toBe(creationTime); - file.write('hello'); + file.writeSync('hello'); expect(file.size).toBe(5); expect(file.creationTime).toBe(creationTime); expect(file.modificationTime).toBeGreaterThan(creationTime!); @@ -266,7 +297,7 @@ describe('expo-file-system behavioral mock', () => { it('File.move updates this.uri and removes the source', async () => { const source = new File(Paths.cache, 'source.txt'); - source.write('payload'); + source.writeSync('payload'); const originalUri = source.uri; const destDir = new Directory(Paths.cache, 'moved'); @@ -285,7 +316,7 @@ describe('expo-file-system behavioral mock', () => { it('File.copy leaves the source intact and copies contents', async () => { const source = new File(Paths.cache, 'copy-src.txt'); - source.write('original'); + source.writeSync('original'); const dest = new File(Paths.cache, 'copy-dest.txt'); await source.copy(dest); @@ -295,7 +326,7 @@ describe('expo-file-system behavioral mock', () => { expect(dest.textSync()).toBe('original'); // Writing to the copy must not mutate the source. - dest.write('mutated'); + dest.writeSync('mutated'); expect(source.textSync()).toBe('original'); expect(dest.textSync()).toBe('mutated'); }); @@ -303,9 +334,9 @@ describe('expo-file-system behavioral mock', () => { it('Directory.delete removes the directory and all descendants', () => { const dir = new Directory(Paths.cache, 'doomed'); dir.create(); - dir.createFile('a.txt', null).write('a'); + dir.createFile('a.txt', null).writeSync('a'); const inner = dir.createDirectory('inner'); - inner.createFile('b.txt', null).write('b'); + inner.createFile('b.txt', null).writeSync('b'); dir.delete(); @@ -361,7 +392,7 @@ describe('expo-file-system behavioral mock', () => { function makeFile(name: string, contents = 'hello'): File { const file = new File(Paths.cache, name); file.create(); - file.write(contents); + file.writeSync(contents); return file; } @@ -401,7 +432,7 @@ describe('expo-file-system behavioral mock', () => { const file = dir.createFile('x.txt', 'text/plain'); expect(file.type).toBe('text/plain'); - file.write('hello'); + file.writeSync('hello'); expect(file.type).toBe('text/plain'); const handle = file.open(FileMode.Truncate); diff --git a/packages/expo-file-system/src/internal/NativeFileSystem.types.ts b/packages/expo-file-system/src/internal/NativeFileSystem.types.ts index 5f6aaf6aa744ba..01f8a6ad20aed6 100644 --- a/packages/expo-file-system/src/internal/NativeFileSystem.types.ts +++ b/packages/expo-file-system/src/internal/NativeFileSystem.types.ts @@ -205,7 +205,13 @@ export declare class NativeFileSystemFile { * Writes content to the file. * @param content The content to write into the file. */ - write(content: string | Uint8Array, options?: FileWriteOptions): void; + write(content: string | Uint8Array, options?: FileWriteOptions): Promise; + + /** + * Writes content to the file. + * @param content The content to write into the file. + */ + writeSync(content: string | Uint8Array, options?: FileWriteOptions): void; /** * Deletes a file. diff --git a/packages/expo-file-system/src/legacyWarnings.ts b/packages/expo-file-system/src/legacyWarnings.ts index 0c2be1d956920e..89d5f0eb0bf795 100644 --- a/packages/expo-file-system/src/legacyWarnings.ts +++ b/packages/expo-file-system/src/legacyWarnings.ts @@ -46,7 +46,7 @@ export async function getContentUriAsync(fileUri: string): Promise { } /** - * @deprecated Use `new File().write()` or import this method from `expo-file-system/legacy`. This method will throw in runtime. + * @deprecated Use `await new File().write()` or `new File().writeSync()` or import this method from `expo-file-system/legacy`. This method will throw in runtime. */ export async function writeAsStringAsync( fileUri: string, From 369b698c5c8a6171e1f6b777e16c4e03dfe3eac4 Mon Sep 17 00:00:00 2001 From: SJvaca30 <122259127+SJvaca30@users.noreply.github.com> Date: Wed, 27 May 2026 17:05:15 +0900 Subject: [PATCH 5/7] [docs][ui] Remove Picker from SDK 55 drop-in replacements (#46325) --- docs/pages/versions/v55.0.0/sdk/picker.mdx | 2 - .../sdk/ui/drop-in-replacements/index.mdx | 1 - .../sdk/ui/drop-in-replacements/picker.mdx | 142 ------------------ .../v55.0.0/expo-ui/community/picker.json | 1 - 4 files changed, 146 deletions(-) delete mode 100644 docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/picker.mdx delete mode 100644 docs/public/static/data/v55.0.0/expo-ui/community/picker.json diff --git a/docs/pages/versions/v55.0.0/sdk/picker.mdx b/docs/pages/versions/v55.0.0/sdk/picker.mdx index 03ac6f04dfb8a8..2af96fb7c468e6 100644 --- a/docs/pages/versions/v55.0.0/sdk/picker.mdx +++ b/docs/pages/versions/v55.0.0/sdk/picker.mdx @@ -12,8 +12,6 @@ import { BookOpen02Icon } from '@expo/styleguide-icons/outline/BookOpen02Icon'; import { APIInstallSection } from '~/components/plugins/InstallSection'; import { BoxLink } from '~/ui/components/BoxLink'; -> **important** [`@expo/ui` provides a drop-in replacement](./ui/drop-in-replacements/picker) for `@react-native-picker/picker`, powered by Jetpack Compose on Android and SwiftUI on iOS. - A component that provides access to the system UI for picking between several options. ## Installation diff --git a/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/index.mdx b/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/index.mdx index 39c64a125ecc9b..e49ac046b607a8 100644 --- a/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/index.mdx +++ b/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/index.mdx @@ -10,5 +10,4 @@ platforms: ['android', 'ios'] The following components provide API-compatible replacements for popular React Native community libraries, powered by `@expo/ui` native components (Jetpack Compose on Android and SwiftUI on iOS). - **[DateTimePicker](datetimepicker)**: Compatible with `@react-native-community/datetimepicker` -- **[Picker](picker)**: Compatible with `@react-native-picker/picker` - **[SegmentedControl](segmentedcontrol)**: Compatible with `@react-native-segmented-control/segmented-control` diff --git a/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/picker.mdx b/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/picker.mdx deleted file mode 100644 index c97051763e8778..00000000000000 --- a/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/picker.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: Picker -description: A picker compatible with @react-native-picker/picker. -sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-ui' -packageName: '@expo/ui' -platforms: ['android', 'ios', 'web'] ---- - -import APISection from '~/components/plugins/APISection'; -import { APIInstallSection } from '~/components/plugins/InstallSection'; - -A `Picker` component with an API compatible with `@react-native-picker/picker`. It uses a SwiftUI wheel `Picker` on iOS, a Material 3 `ExposedDropdownMenuBox` on Android, and a native ``"},{"kind":"text","text":" element."}]},"typeParameters":[{"name":"T","variant":"typeParam","kind":131072,"type":{"type":"reference","name":"PickerItemValue","package":"@expo/ui"}}],"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","typeArguments":[{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}],"name":"PickerProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]},{"name":"default","variant":"reference","kind":4194304}],"packageName":"@expo/ui"} \ No newline at end of file From 202143659e34274b2f59a8e32bd66ab95abc0829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20=C5=9Awierk?= <58403334+sswrk@users.noreply.github.com> Date: Wed, 27 May 2026 11:04:42 +0200 Subject: [PATCH 6/7] [docs] EAS workflows Apple Device Registration Request jobs documentation (#46296) # Why EAS Workflows supports an `apple-device-registration-request` pre-packaged job that pauses a run until a tester enrolls an iOS device and a team member approves it on expo.dev. That flow is useful for [internal distribution](/build/internal-distribution/) but was not documented alongside other workflow jobs. This PR documents the job so teams can automate device registration in workflows instead of running `eas device:create` manually and coordinating builds separately. # How - Added an Apple device registration request section to pre-packaged jobs docs. - Added a short `apple-device-registration-request` entry in workflow syntax jobs and listed it among jobs that do not run on a VM (so `env` does not apply). - Cross-linked from internal distribution under "Managing devices", pointing readers to this job and pairing it with a `build` job that refreshes the ad hoc provisioning profile. # Test Plan image # 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) --------- Co-authored-by: Aman Mittal --- docs/pages/build/internal-distribution.mdx | 2 + .../pages/eas/workflows/pre-packaged-jobs.mdx | 97 +++++++++++++++++++ docs/pages/eas/workflows/syntax.mdx | 16 ++- 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/docs/pages/build/internal-distribution.mdx b/docs/pages/build/internal-distribution.mdx index 8e08a7a378ecd4..fd149c99e75e7e 100644 --- a/docs/pages/build/internal-distribution.mdx +++ b/docs/pages/build/internal-distribution.mdx @@ -56,6 +56,8 @@ Otherwise, after registering a device with [`eas device:create`](/eas/cli/#eas-d ### Managing devices +With [EAS Workflows](/eas/workflows/get-started), you can pause a workflow until a tester enrolls an iOS device and a team member approves it using the [`apple-device-registration-request`](/eas/workflows/pre-packaged-jobs#apple-device-registration-request) job. Pair it with a `build` job that sets `refresh_ad_hoc_provisioning_profile: true` to include the new device in an internal distribution build. + You can see any devices registered via `eas device:create` by running: +## Apple device registration request + +Pause a workflow run until an iOS device enrolls for a specific [Apple Team](https://expo.fyi/apple-team) and a team member approves that enrollment on [expo.dev](https://expo.dev). Use this job to automate registering a device for [internal distribution](/build/internal-distribution/) inside EAS Workflows. + +When the workflow reaches this job, the job and the run enter `action-required` and stay paused until the flow completes: + +1. The workflow run page shows a QR code and a registration link for the device being enrolled. +2. On the iPhone or iPad, the device downloads a provisioning profile and installs it through Settings. Only one profile is ready at a time, and it is removed if not installed within eight minutes. +3. After installation, the unique device identifier (UDID) and metadata are collected. +4. On the workflow run page, a team member approves or rejects the enrollment. Approval marks the job successful and exposes outputs (UDID, model, and more) to downstream jobs. Rejection fails the job and blocks jobs that use `needs`. + +If the UDID is already registered on your account, the job still waits for enrollment and approval before continuing. + +### Syntax + +```yaml +jobs: + register_device: + type: apple-device-registration-request + params: + apple_team_identifier: string # optional +``` + +#### Parameters + +You can pass the following parameters into the `params` list: + +| Parameter | Type | Description | +| --------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| apple_team_identifier | string | Optional. The Apple Team ID (for example, `ABCDE12345`). If you omit this parameter and your Expo account has exactly one Apple Team, that team is used. You must set this parameter when your account has no Apple Teams or has two or more Apple Teams. When provided, EAS resolves or creates the team from the identifier. | + +#### Outputs + +After a team member approves the enrolled device, you can reference the following outputs in subsequent jobs: + +| Output | Type | Description | +| ---------------- | ------ | ---------------------------------------------------------------- | +| apple_device_id | string | Expo internal device ID. | +| identifier | string | Device UDID. | +| name | string | Device name. May be empty. | +| model | string | Hardware model string (for example, `iPhone11,2`). May be empty. | +| device_class | string | Device class: `iphone`, `ipad`, or `mac`. May be empty. | +| software_version | string | iOS version string. May be empty. | + +If a team member rejects the enrollment, the job fails and does not set outputs. Downstream jobs that use `needs` will not run. + +### Examples + +Here are some practical examples of using the Apple device registration request job: + + + +This workflow registers an iOS device and sends a Slack message with the device UDID and model from the job outputs. + +```yaml .eas/workflows/register-device-slack.yml +name: Register iOS device and notify Slack + +jobs: + register_device: + type: apple-device-registration-request + params: + apple_team_identifier: XX33YYZ44Z + notify_slack: + needs: [register_device] + type: slack + params: + webhook_url: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX + message: 'Registered device ${{ needs.register_device.outputs.identifier }} (${{ needs.register_device.outputs.model }})' +``` + + + + + +This workflow registers a device and then runs an iOS internal distribution build that refreshes the ad hoc provisioning profile so the new device is included. + +```yaml .eas/workflows/register-device-build.yml +name: Register device and build iOS internal + +jobs: + register_device: + type: apple-device-registration-request + params: + apple_team_identifier: ABCDE12345 + build_ios: + needs: [register_device] + type: build + params: + platform: ios + profile: preview + refresh_ad_hoc_provisioning_profile: true +``` + +The `preview` profile must set [`distribution: internal`](/eas/json/#distribution) and use [credentials managed by EAS](/app-signing/app-credentials/). For CI, you also need an App Store Connect API key. See [internal distribution on CI](/build/internal-distribution/#automation-on-ci-optional) and [trigger builds from CI](/build/building-on-ci/). + + + ## Require Approval Require approval from a user before continuing with the workflow. A user can approve or reject which translates to success or failure of the job. diff --git a/docs/pages/eas/workflows/syntax.mdx b/docs/pages/eas/workflows/syntax.mdx index 09f9c1ad7b7a6f..d90e08ab96bed9 100644 --- a/docs/pages/eas/workflows/syntax.mdx +++ b/docs/pages/eas/workflows/syntax.mdx @@ -428,7 +428,7 @@ jobs: ### `jobs..env` -Sets environment variables for the job. The property is available on all jobs running a VM (all jobs except for the pre-packaged `require-approval`, `doc`, `get-build` and `slack` jobs). +Sets environment variables for the job. The property is available on all jobs running a VM (all jobs except for the pre-packaged `apple-device-registration-request`, `require-approval`, `doc`, `get-build`, and `slack` jobs). ```yaml jobs: @@ -1358,6 +1358,20 @@ This job outputs the following properties: } ``` +#### `apple-device-registration-request` + +Pauses the workflow until an iOS device enrolls for an Apple Team and a team member approves the enrollment. See [Apple device registration request job documentation](/eas/workflows/pre-packaged-jobs#apple-device-registration-request) for detailed information and examples. + +```yaml +jobs: + register_device: + # @info # + type: apple-device-registration-request + params: + apple_team_identifier: string # optional + # @end # +``` + #### `require-approval` Requires approval from a user before continuing with the workflow. A user can approve or reject which translates to success or failure of the job. See [Require Approval job documentation](/eas/workflows/pre-packaged-jobs#require-approval) for detailed information and examples. From b47d7b3d6d23b23a91826938de03184136d4d88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosmaty?= Date: Wed, 27 May 2026 11:39:49 +0200 Subject: [PATCH 7/7] [expo-dev-launcher][android] Fix NPE on deep link cold launch (#46328) --- packages/expo-dev-launcher/CHANGELOG.md | 1 + .../devlauncher/DevLauncherController.kt | 46 +++++++++++-------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/expo-dev-launcher/CHANGELOG.md b/packages/expo-dev-launcher/CHANGELOG.md index a1df921e1e2f37..8e4b297168df26 100644 --- a/packages/expo-dev-launcher/CHANGELOG.md +++ b/packages/expo-dev-launcher/CHANGELOG.md @@ -9,6 +9,7 @@ ### 🐛 Bug fixes - [iOS] Cleared the deep-link URL from cached `launchOptions` after it is consumed ([#46265](https://github.com/expo/expo/pull/46265) by [@gabrieldonadel](https://github.com/gabrieldonadel)) +- [Android] Fixed a crash when cold-launching a development build from a deep link that carries intent categories (e.g. an App Link opened from a browser). ([#46314](https://github.com/expo/expo/pull/46314) by [@lilianchiassai-fc](https://github.com/lilianchiassai-fc) & [#46328](https://github.com/expo/expo/pull/46328) by [@lukmccall](https://github.com/lukmccall)) ### 💡 Others diff --git a/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt b/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt index 53f2e2b2a48126..43ed3f2e51fe1e 100644 --- a/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt +++ b/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt @@ -413,30 +413,38 @@ class DevLauncherController private constructor( Intent(context, DevLauncherActivity::class.java) .apply { addFlags(NEW_ACTIVITY_FLAGS) } - private fun createAppIntent() = - createBasicAppIntent().apply { - pendingIntentRegistry - .consumePendingIntent() - ?.let { intent -> - action = intent.action - data = intent.data - intent.extras?.let { - putExtras(it) - } - intent.categories?.let { - categories.addAll(it) - } - } ?: run { - // If no pending intent is available, use the extras from the intent that was used to launch the app. - pendingIntentExtras?.let { - putExtras(it) - } + private fun createAppIntent(): Intent { + val newIntent = createBasicAppIntent() + val pendingIntent = pendingIntentRegistry + .consumePendingIntent() + + if (pendingIntent != null) { + newIntent.action = pendingIntent.action + newIntent.data = pendingIntent.data + + val pendingExtras = pendingIntent.extras + if (pendingExtras != null) { + newIntent.putExtras(pendingExtras) } - // Clear the pending intent extras after using them. + val pendingCategories = pendingIntent.categories + if (pendingCategories != null) { + pendingCategories.forEach { pendingCategory -> + newIntent.addCategory(pendingCategory) + } + } + } else { + // If no pending intent is available, use the extras from the intent that was used to launch the app. + val extras = pendingIntentExtras + if (extras != null) { + newIntent.putExtras(extras) + } pendingIntentExtras = null } + return newIntent + } + private fun createBasicAppIntent() = if (sLauncherClass == null) { checkNotNull(