Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4e2d728
Fix `BottomSheet` with a custom background making the Android system …
expo-bot Aug 27, 2026
b908c11
[android][expo-go] Build the codegen CLI when the submodule is missin…
alanjhughes Aug 27, 2026
36a2ad6
[ios][notifications] Support local notification grouping via threadId…
vonovak Aug 27, 2026
813f081
[docs] Match feedback dialog radius with the EAS modal (#49389)
amandeepmittal Aug 27, 2026
6acec3b
[font][web] Match @font-face rules by the rule's bare family name, fi…
vonovak Aug 27, 2026
2e53c2c
[router] Abandon and abort loader requests removed by navigation (#48…
hassankhan Aug 27, 2026
2180669
[log-box] Show LogBox for fatal errors reported off the main thread (…
hassankhan Aug 27, 2026
7bf9e8f
[docs] Sync upstream reference content (EAS CLI reference, other) (#4…
expo-bot Aug 27, 2026
57eee4b
[docs] document observe memory warning (#49407)
kadikraman Aug 27, 2026
50520df
[app-intents][ios][2/n] Add cli tool (#47223)
behenate Aug 27, 2026
72ef4df
[app-intents][docs][3/n] Add docs (#47226)
behenate Aug 27, 2026
81d36e5
[app-intents][ncl][4/n] Add example NCL screen (#47228)
behenate Aug 27, 2026
622a2ff
[docs] Add description, contact point, and address to structured data…
amandeepmittal Aug 27, 2026
a6566f2
[docs] Add a when-to-use section to llms.txt (#49440)
amandeepmittal Aug 27, 2026
40f5355
[docs] Recommend Software Mansion editors for rich text editing (#49410)
amandeepmittal Aug 27, 2026
89e3934
[docs] Soften floating surface radii off the 6px default (#49387)
amandeepmittal Aug 27, 2026
5a8f739
[docs] Serve smaller YouTube thumbnails and lazy-load `Talks` cards (…
amandeepmittal Aug 27, 2026
1510062
[docs] Pick the terminal package manager in CSS, not after hydration …
amandeepmittal Aug 27, 2026
589ee55
[splash-screen] Generate Android fallback drawable without an image (…
expo-tuft[bot] Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions apps/bare-expo/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
"expo-router",
"expo-video",
"expo-background-fetch",
[
"expo-app-intents",
{
"directory": "../native-component-list/app-intents"
}
],
[
"expo-splash-screen",
{
Expand Down Expand Up @@ -119,8 +125,10 @@
"inlineModules": {
"watchedDirectories": [
"../native-component-list/src/screens/inlineModules/inlineModulesExamples",
"../native-component-list/src/screens/ModulesCore"
]
"../native-component-list/src/screens/ModulesCore",
"../native-component-list/app-intents"
],
"xcodeProjectTargets": ["BareExpo"]
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions apps/bare-expo/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ const getSearchScreenOptions = optionalRequire(() =>
require('native-component-list/src/screens/SearchScreen')
)?.getSearchScreenOptions;

const AppIntentsNavigationHandler = optionalRequire(() =>
require('native-component-list/src/screens/AppIntents/AppIntentsNavigationHandler')
)?.AppIntentsNavigationHandler;

function useLoaded() {
const [isLoaded, setLoaded] = React.useState(false);
React.useEffect(() => {
Expand Down Expand Up @@ -100,6 +104,7 @@ function RootLayout() {
<Stack.Screen name="redirect" />
<Stack.Screen name="search" options={getSearchScreenOptions?.(theme)} />
</Stack>
{AppIntentsNavigationHandler ? <AppIntentsNavigationHandler /> : null}
<StatusBar style={themeName === 'light' ? 'dark' : 'light'} />
</GestureHandlerRootView>
);
Expand Down
13 changes: 13 additions & 0 deletions apps/bare-expo/ios/BareExpo.xcodeproj/project.pbxproj

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/bare-expo/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4048,7 +4048,7 @@ SPEC CHECKSUMS:
expo-dev-menu-interface: 7a59e803916fbfd1b96c75ff91ea8dddac7e722f
ExpoAgeRange: f30bef82f1a99c097e28f50ed41ca04d5e1cab1c
ExpoAppIntegrity: c4f6d2065674ec9744881f8c04d52ade9b6cacd8
ExpoAppIntents: e4702a43af248c7f72ffe4855635f8d0319d330a
ExpoAppIntents: ca59c27e6cb5093cc05e82a44211d8595d25e91d
ExpoAppleAuthentication: 97d113f0f2680067e6e76d3c9249e7e1fc026b42
ExpoAppMetrics: edb2e75dba50059ba781c56b8ff0824aaeb9e74b
ExpoAsset: a76534cf7b762978861dd31708308496c676166f
Expand Down
4 changes: 2 additions & 2 deletions apps/bare-expo/ios/Podfile.properties.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"expo.jsEngine": "hermes",
"newArchEnabled": "true",
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
"expo.inlineModules.watchedDirectories": "[\"../native-component-list/src/screens/inlineModules/inlineModulesExamples\",\"../native-component-list/src/screens/ModulesCore\"]",
"expo.inlineModules.xcodeProjectTargets": "{\"mainTarget\":\"BareExpo\",\"targets\":[]}",
"expo.inlineModules.watchedDirectories": "[\"../native-component-list/src/screens/inlineModules/inlineModulesExamples\",\"../native-component-list/src/screens/ModulesCore\",\"../native-component-list/app-intents\"]",
"expo.inlineModules.xcodeProjectTargets": "{\"targets\":[\"BareExpo\"]}",
"ios.buildReactNativeFromSource": "false",
"EXPO_USE_PRECOMPILED_MODULES": "false"
}
14 changes: 14 additions & 0 deletions apps/expo-go/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ project(":packages:react-native:ReactAndroid").afterEvaluate { reactAndroid ->
reactAndroid.tasks.named("buildCodegenCLI").configure { it.enabled = false }
}

// Every module's generateCodegenSchemaFromJavaScript reads react-native-codegen/lib
// directly, so build it once when it is missing.
def labCodegenDir = file("$rootDir/../../../react-native-lab/react-native/packages/react-native-codegen")
tasks.register("buildLabCodegenCli", Exec) {
onlyIf { !new File(labCodegenDir, "lib/cli/combine/combine-js-to-schema-cli.js").exists() }
workingDir labCodegenDir
commandLine "bash", "scripts/oss/build.sh"
}
allprojects {
tasks.matching { it.name == "generateCodegenSchemaFromJavaScript" }.configureEach {
it.dependsOn(":buildLabCodegenCli")
}
}

// Expo Go ships a release build that keeps the JS debugger. React Native gates those native defines
// behind REACT_NATIVE_DEBUG_OPTIMIZED, and its CMake guard only passes automatically when
// CMAKE_BUILD_TYPE is Debug. Release maps to RelWithDebInfo, which does not match, so set the flag
Expand Down
19 changes: 19 additions & 0 deletions apps/native-component-list/app-intents/AppIntentsSetup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
internal import ExpoAppIntents
internal import ExpoModulesCore

/// Registered Expo inline module that wires app-target App Intents code to expo-app-intents.
/// Do not change the name of this class.
final class AppIntentsSetup: Module {
public func definition() -> ExpoModulesCore.ModuleDefinition {
Name("AppIntentsSetup")

OnCreate {
Task {
await AppIntentDispatcher.shared.setShortcutsRefreshHandler {
AppShortcuts.updateAppShortcutParameters()
}
AppShortcuts.updateAppShortcutParameters()
}
}
}
}
33 changes: 33 additions & 0 deletions apps/native-component-list/app-intents/AppShortcuts.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import AppIntents

/// All App Shortcuts for this app. Phrases are compiled into the app and cannot be created
/// at runtime. Required parameters that are not in the launch phrase are collected by Siri
/// as follow-up questions.
///
/// System rules:
/// - Every phrase must include \(.applicationName) or the phrase is dropped at build time.
/// - At most 10 App Shortcuts per app. Apple recommends 2-5.
struct AppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: IncreaseCounterIntent(),
phrases: [
"Increase the counter in \(.applicationName)"
],
shortTitle: "Increase Counter",
systemImageName: "plus.circle"
)

AppShortcut(
intent: OrderFoodIntent(),
phrases: [
"Place an order in \(.applicationName)",
"Order food in \(.applicationName)",
"Order \(\.$dish) in \(.applicationName)",
"Place an order for \(\.$dish) in \(.applicationName)",
],
shortTitle: "Place an order",
systemImageName: "fork.knife"
)
}
}
46 changes: 46 additions & 0 deletions apps/native-component-list/app-intents/CreateDraftIntent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import AppIntents
internal import ExpoAppIntents
import Foundation

/// On devices running the new AI Siri, the `CreateDraftIntent` should be automatically
/// picked up by the system without the need of registering it in AppShortcutProvider phrases.
@available(iOS 18.0, *)
@AppIntent(schema: .mail.createDraft)
struct CreateDraftIntent {
static let openAppWhenRun: Bool = true

var body: AttributedString?
var to: [IntentPerson]
var subject: String?
var cc: [IntentPerson]
var bcc: [IntentPerson]
var account: MailAccountEntity?
var attachments: [IntentFile]

@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<MailDraftEntity> {
let draft = MailDraftEntity(
id: UUID().uuidString,
to: to,
cc: cc,
bcc: bcc,
subject: subject,
body: body,
attachments: attachments,
account: account ?? MailAccountEntity.default
)

await AppIntentDispatcher.shared.dispatch(
name: "createMailDraft",
params: [
"id": .string(draft.id),
"subject": .string(subject ?? ""),
"body": .string(draft.bodyText),
"recipients": .array(draft.recipientAddresses.map(AppIntentValue.string)),
"attachmentCount": .int(attachments.count),
]
)

return .result(value: draft)
}
}
30 changes: 30 additions & 0 deletions apps/native-component-list/app-intents/DeleteDraftIntent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import AppIntents
internal import ExpoAppIntents
import Foundation

/// Deletes one or more drafts. The `.mail.deleteDraft` schema supplies its own confirmation
/// dialog ("Are you sure you would like to delete ... ?"), so this intent does not ask again,
/// and it does not open the app: the invocation is queued and JavaScript applies it the next
/// time it runs.
///
/// The schema requires an authentication policy at least as restrictive as
/// `.requiresLocalDeviceAuthentication`, because deleting content is destructive.
@available(iOS 18.0, *)
@AppIntent(schema: .mail.deleteDraft)
struct DeleteDraftIntent {
static let authenticationPolicy: IntentAuthenticationPolicy = .requiresLocalDeviceAuthentication

var entities: [MailDraftEntity]

@MainActor
func perform() async throws -> some IntentResult {
await AppIntentDispatcher.shared.dispatch(
name: "deleteMailDrafts",
params: [
"ids": .array(entities.map { .string($0.id) })
]
)

return .result()
}
}
64 changes: 64 additions & 0 deletions apps/native-component-list/app-intents/Entities/DishEntity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import AppIntents
internal import ExpoAppIntents
import Foundation

private func normalizeDishText(_ value: String) -> String {
return
value
.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current)
.trimmingCharacters(in: .whitespacesAndNewlines)
}

struct DishEntity: AppEntity {
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Dish"
static let defaultQuery = DishQuery()

var id: String
@Property(title: "Name")
var name: String
var subtitle: String?
var synonyms: [String]

var displayRepresentation: DisplayRepresentation {
if let subtitle {
if #available(iOS 17.0, *) {
return DisplayRepresentation(
title: "\(name)",
subtitle: "\(subtitle)",
synonyms: synonyms.map { LocalizedStringResource(stringLiteral: $0) }
)
}
return DisplayRepresentation(title: "\(name)", subtitle: "\(subtitle)")
}

if #available(iOS 17.0, *) {
return DisplayRepresentation(
title: "\(name)",
synonyms: synonyms.map { LocalizedStringResource(stringLiteral: $0) }
)
}
return DisplayRepresentation(title: "\(name)")
}

init(record: AppIntentEntityRecord) {
self.id = record.id
self.subtitle = record.subtitle
self.synonyms = record.synonyms
self.name = record.title
}

private var searchableTerms: [String] {
return [id, name] + synonyms
}

func matches(_ searchText: String) -> Bool {
let normalizedSearch = normalizeDishText(searchText)
guard !normalizedSearch.isEmpty else {
return false
}

return searchableTerms.map(normalizeDishText).contains { term in
normalizedSearch == term || normalizedSearch.contains(term) || term.contains(normalizedSearch)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import AppIntents
import Foundation

/// The `.mail.draft` schema requires a non-optional account, so every draft needs one. This
/// example ships a single hardcoded account. A real mail app would publish its accounts to the
/// entity catalog from JavaScript, the same way the restaurant example publishes dishes.
@available(iOS 18.0, *)
@AppEntity(schema: .mail.account)
struct MailAccountEntity {
static let defaultQuery = MailAccountEntityQuery()

static let `default` = MailAccountEntity(
id: "primary",
name: "Primary",
emailAddress: "me@example.com"
)

var id: String
var name: String
var emailAddress: String

init(id: String, name: String, emailAddress: String) {
self.id = id
self.name = name
self.emailAddress = emailAddress
}

var displayRepresentation: DisplayRepresentation {
return DisplayRepresentation(title: "\(name)", subtitle: "\(emailAddress)")
}
}
Loading
Loading