Feature Requests

With Expo, you can write iOS and Android experiences in JavaScript using React Native.
expo-observe / expo-app-metrics: build-time opt-out (disabled until configured) and a switch for crash capture
What we need A build-time way to start with dispatching disabled: a config plugin option, or an Info.plist / AndroidManifest key the native side reads before JS runs. Dispatching would turn on only when the app calls configure at runtime. A way to turn crash capture off, at build time or through the same configuration: the JVM handler on Android and the MetricKit crash diagnostics on iOS. On Android, install the OkHttp client hook only when network tracing is enabled. Why Our app sends usage data only after the user consents. With expo-observe 58.0.19 and expo-app-metrics 58.0.15, a fresh install can send and collect data before any JS runs, so before the app can ask for consent: Dispatching defaults to on when no configuration is stored: ObservabilityManager.kt:389 (config?.dispatchingEnabled ?: true) and Observability.swift:385 (config?.dispatchingEnabled ?? true). The configuration comes only from the JS configure call. It is persisted (UserDefaults / SharedPreferences). Neither package has an app.plugin.js, and we found no plist or manifest key that the native code reads. On iOS, ObserveAppDelegateSubscriber.applicationWillResignActive (ObserveAppDelegateSubscriber.swift:7) triggers a dispatch independently of JS. Crash capture has no off switch. Android: JvmCrashHandler.install runs unconditionally in AppMetricsApplicationLifecycleListeners.kt:19. iOS: the MetricKit subscriber is registered at launch, and crashDiagnostics is mapped to crash reports (MetricKitSubscriber.swift:31). Android: OkHttpClientProviderHook.installIfNeeded() (AppMetricsApplicationLifecycleListeners.kt:28) is installed unconditionally. networkTraces: false stops the recording but leaves the hook in place. Patching the native defaults locally would work today, but every SDK upgrade could silently undo it. A supported opt-out would let apps with a consent requirement adopt these packages. Environment: Expo SDK 58 (expo 58.0.22, expo-observe 58.0.19, expo-app-metrics 58.0.15), iOS and Android, Continuous Native Generation (expo prebuild, native folders not committed).
0
[@expo/ui][expo-widgets] Text: support iOS 18's live date formats (SystemFormatStyle) so a Live Activity can count down in hours and minutes without seconds
@expo/ui 's SwiftUI Text can show a self-updating date only through timerInterval ( Text(timerInterval:countsDown:) ) or date + dateStyle ( .timer , .relative , .offset ). In a Live Activity rendered by expo-widgets , every one of these either shows seconds or carries a sign. Measured 42 m 30 s before a date, read 1 to 2 minutes later (unlocked in Notification Center / on the locked Lock Screen): timerInterval + countsDown : 40:39 / 41:-- dateStyle="timer" : 40:38 / 41 minutes dateStyle="relative" : 40 min, 38 sec (compact Dynamic Island: 40 min, 49… ) / 41 min dateStyle="offset" : −40 minutes / −41 minutes ProgressView timerInterval (its value label): 40:39 / 41:-- So once the phone is unlocked (Face ID on the Lock Screen, Notification Center, the Dynamic Island), every option shows seconds except offset , which always carries a minus sign and offers no control over the sign or the fields. iOS 18 added live-updating system formats that WidgetKit and ActivityKit render without timeline reloads or pushes: Text(_ source: TimeDataSource<Date>, format: some DiscreteFormatStyle) with SystemFormatStyle.DateOffset , .DateReference , .Timer and .Stopwatch . Apple introduced them at WWDC24 ("What's new in SwiftUI") for widgets and Live Activities. With allowedFields: [.hour, .minute] they never show seconds, for example 2 hr, 15 min or 42 min . With .textVariant(.sizeDependent) they also narrow to 2h 15m / 42m in a compact Dynamic Island. Neither @expo/ui 58.0.28 nor expo-widgets 58.0.28 exposes them. And because expo-widgets renders a closed set of @expo/ui views in the extension ( ios/Widgets/ViewRenderer.swift ), an app can't add its own SwiftUI view to fill the gap. The same gap shows on ProgressView(timerInterval:countsDown:) . @expo/ui uses the label: initializer, which shows SwiftUI's default current-value label ( 42:30 , with seconds), and there's no prop to hide it. Repro An SDK 58 app with @expo/ui@58.0.28 and expo-widgets@58.0.28 (no patches), and this Live Activity: ```tsx import { HStack, Image, ProgressView, Text, VStack } from '@expo/ui/swift-ui'; import { createLiveActivity } from 'expo-widgets'; const Countdown = createLiveActivity<{ fromMs: number; atMs: number }>('Countdown', (props) => { 'widget'; const from = new Date(props.fromMs); const at = new Date(props.atMs); return { banner: ( <VStack alignment="leading"> <HStack><Text>timerInterval</Text><Text timerInterval={{ lower: from, upper: at }} countsDown /></HStack> <HStack><Text>dateStyle timer</Text><Text date={at} dateStyle="timer" /></HStack> <HStack><Text>dateStyle relative</Text><Text date={at} dateStyle="relative" /></HStack> <HStack><Text>dateStyle offset</Text><Text date={at} dateStyle="offset" /></HStack> <ProgressView timerInterval={{ lower: from, upper: at }} countsDown /> </VStack> ), compactLeading: <Image systemName="timer" />, compactTrailing: <Text date={at} dateStyle="relative" />, minimal: <Image systemName="timer" />, }; }); const fromMs = Date.now (); Countdown.start({ fromMs, atMs: fromMs + 42.5 * 60_000 }); Look at the Lock Screen while locked, then unlocked (Notification Center), and at the compact Dynamic Island from the Home Screen. You get the readings above. There's no way, through either package, to read `42 min` (or `2h 15m`) and keep counting without a push per minute. **Proposed API** On `Text` (iOS 18+; below 18 it falls back to the existing `date` / `dateStyle` / `timerInterval` props, so a layout can pass both): ```ts type TextLiveFormat = | { kind: 'offset'; to: Date; allowedFields?: ('hour' | 'minute' | 'second')[]; sign?: 'automatic' | 'never' | 'always' } | { kind: 'reference'; to: Date; allowedFields?: ('day' | 'hour' | 'minute' | 'second')[]; maxFieldCount?: number } | { kind: 'timer'; countingDownIn: { lower: Date; upper: Date }; maxPrecisionSeconds?: number }; <Text liveFormat={{ kind: 'offset', to: start, allowedFields: ['hour', 'minute'], sign: 'never' }} /> Plus a textVariant modifier ( 'sizeDependent' | 'preferredFormat' ), mapping to .textVariant(_:) . On ProgressView : hidesCurrentValueLabel?: boolean , mapping to init(timerInterval:countsDown:label:currentValueLabel: { EmptyView() }) . Swift side, in TextView.swift 's buildText : ```swift if #available(iOS 18.0, *), let live = props.liveFormat { switch live.kind { case "offset": return Text(.currentDate, format: .offset(to: live.to , allowedFields: live.fields, sign: live.sign)) case "timer": return Text(.currentDate, format: .timer(countingDownIn: live.lower..<live.upper, maxPrecision: .seconds(live.precision))) // reference, ... } } ``` One detail from doing it: .textVariant(_:) is declared on Text and returns some View , so it has to be applied to the Text before the view-modifier array (which erases to a View), while buildText stays a Text for nested-Text concatenation. We carry the offset arm (with sizeDependent ) and the hidden ProgressView label as a patch-package patch today, across ios/TextView.swift , ios/ProgressView.swift and the two TS components. We'd gladly open the PR if the shape is welcome. Why Event and travel apps show "starts in" countdowns on the lock screen, where seconds are noise. On a locked iPhone the system replaces them with dashes ( 42:-- ), which reads as broken. Apple's own guidance for Live Activities is to update rarely and let the system tick. The iOS 18 formats are exactly that, and today they're unreachable from Expo. Environment Expo SDK 58 (expo 58.0.22), @expo/ui 58.0.28, expo-widgets 58.0.28 (both unpatched), Xcode 27.1, iPhone 17 simulator on iOS 27.
0
Read optimizeGraph and treeShaking from the app config experiments, not only from EXPO_UNSTABLE_* variables
Graph optimization and tree shaking turn on only through EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH and EXPO_UNSTABLE_TREE_SHAKING ( https://docs.expo.dev/guides/tree-shaking/ ). Gradle's createBundleReleaseJsAndAssets runs expo export:embed without either unless every shell, IDE and CI job that builds the app exports both, so the same app ships a shaken bundle from one build path and the whole graph from another. I'd like the CLI to read both from the app config, as it reads experiments.reactCompiler, and experiments.reactServerFunctions with EXPO_UNSTABLE_SERVER_FUNCTIONS as its fallback: { "expo": { "experiments": { "optimizeGraph": true, "treeShaking": true } } } with the variables still turning them on as they do now, and @expo/config-types declaring the two keys. Today app.json's experiments change nothing. https://gist.github.com/YevheniiKotyrlo/d98c063ebd72232bafb2694bd5d5cfb0 bundles a two-file app with expo export:embed, once with only app.json's experiments and once with the variables (npm install && node run.mjs): expo 58.0.24, @expo/cli 58.1.20, node v26.12.1, win32 10.0.26300 --- app.json experiments only: expo export:embed --platform android --dev false --minify true --entry-file index.js exit status 0 UNUSED_EXPORT_MARKER in the bundle: yes --- EXPO_UNSTABLE_* variables: expo export:embed --platform android --dev false --minify true --entry-file index.js EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 EXPO_UNSTABLE_TREE_SHAKING=1 Experimental bundle optimization is enabled. Experimental tree shaking is enabled. exit status 0 UNUSED_EXPORT_MARKER in the bundle: no SUMMARY: defect shows: with only app.json's experiments the CLI announces 0 of 2 experiments and the bundle keeps UNUSED_EXPORT_MARKER; with the variables it announces 2 of 2 and drops it On main (b08d46ec), both flags still come only from the environment: https://github.com/expo/expo/blob/b08d46ec7753d89991260598cf545210675d57fb/packages/@expo/cli/src/start/server/metro/instantiateMetro.ts#L304-L315 https://github.com/expo/expo/blob/b08d46ec7753d89991260598cf545210675d57fb/packages/@expo/cli/src/start/server/middleware/metroOptions.ts#L104-L113
0
Siri AI Developer Preview support
I tested Apple's Siri AI Developer Preview (iOS 27.3 beta) with expo-app-intents 0.5.16 on Expo SDK 58, on a real iPhone. The package works unchanged. Siri called tools written in app-intents/ , including with the app closed and with the phone locked. A few preview-specific pieces needed workarounds, and I'd like to know whether you'd welcome contributions for them. Reproduction and write-up: https://github.com/airowe/siri-ai-expo Setup: Xcode 27.3 beta (27C5020n), iPhone 16 Pro on iOS 27.3 beta (24C5037i) with Siri AI on, Release build signed for development. ### What worked with no package changes @AppIntent(.tool) intents in app-intents/ . Siri called them and spoke their dialog, with the app open, with it closed, and with the phone locked. @AppEntity(.generable) parameters filled in by Siri. Entities published from JavaScript with setEntityCatalogAsync and reindexEntitiesAsync , found by Siri through Spotlight. A tool that calls an authenticated API and refreshes an expired token inside the Siri request. ### Where I needed workarounds Compiling the tool code. The tool macros only exist in the 27.3 SDK. I gated them behind a Swift compilation condition set by my own config plugin: ```js entry.buildSettings.SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SIRI_AI_PREVIEW" ; ``` Deployment target. @Property(title:description:) is iOS 27.3+ only, and a property wrapper can't be limited to newer versions with an availability check. Any entity that uses it raises the whole app to iOS 27.3. Prebuild. expo prebuild regenerates ios/ , so I need a script to re-add the Siri Evaluations test target after every prebuild. A Skills Bundle added in Xcode is presumably lost the same way (not yet verified). Siri answers from indexed data instead of calling the tool. This is the one I'd most like to see in the docs: - With order status in the entity and its Spotlight description, Siri called the status tool in 3 of 8 runs. In the rest it gave the same facts in its own words, from the index. - With only the order's identity indexed (id, name, synonyms) and the tool reading status when it runs: 4 of 4 completed runs. - A tool with nothing indexed (a cart fetched from an API): 5 of 5 . - The rule: index what identifies a record, never the answer the tool exists to give. Locked phone. Tools that read a token need Keychain items readable after first unlock ( kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ). With that, Siri read an authenticated result with the phone locked. Testing. Xcode 27.3 ships SiriEvaluations.framework for devices only, not for the Simulator. ### Question Would you welcome contributions for any of these, or would you rather wait until the APIs leave beta? A config plugin option that sets the preview compilation condition and keeps the Siri Evaluations target and Skills Bundle through prebuild. A tool example for npx expo-app-intents init . Guidance in the docs and the bundled agent skill, covering points 4 and 5. I'm also following runBackgroundTask (#51595), which covers tools answering from JavaScript, so I've left that out.
0
Load More
→