Native voice assistant integration for Expo apps with Siri and Google Assistant support.
- 🔧 Config Plugin — auto-wires Info.plist, entitlements, AndroidManifest, shortcuts.xml, deep-link verification
- 📱 Cross-platform TypeScript API — single
VoiceAssistant+VoiceIntentBuilderfor iOS and Android - 🎯 Typed intent builder — generic-parametrized; compiler enforces handler signatures
- 🧪 Tested across layers — JS unit + plugin assertions + bridge contracts + iOS XCTest + Android JUnit/Robolectric
This package ships today:
- Full config-plugin setup (one
app.jsonblock configures iOS + Android native voice integration) - TypeScript intent declaration API and fluent builder
donateIntent()for iOS Siri Suggestions (NSUserActivity)- Dynamic shortcuts on Android (ShortcutManager)
Still in development (native scaffolding present, runtime not fully wired):
- Siri / App Intents invocation → JS handler callback (iOS 16+ AppShortcutsProvider bridge)
- Google Assistant / App Actions invocation → JS handler callback (Android intent forwarding)
In other words: the manifest plumbing is done; the bidirectional runtime bridge is the next milestone.
npx expo install expo-assistantAdd the config plugin to your app.json or app.config.js:
{
"expo": {
"plugins": [
[
"expo-assistant",
{
"intents": ["search", "media", "productivity"],
"enableSiriKit": true,
"enableAppActions": true
}
]
]
}
}import { VoiceAssistant, VoiceIntentBuilder, IntentCategory } from 'expo-assistant';
// Initialize voice assistant
const assistant = await VoiceAssistant.initialize();
// Create a search intent
const searchIntent = VoiceIntentBuilder
.create<{ query: string }>()
.withId('search-products')
.withCategory(IntentCategory.SEARCH)
.requiredParameter('query', {
type: ParameterType.STRING,
prompt: 'What would you like to search for?'
})
.withHandler({
handle: async (params) => {
const results = await searchProducts(params.query);
return { success: true, data: results };
}
})
.build();
// Register the intent
await assistant.registerIntent(searchIntent);
// Request permissions
await assistant.requestMicrophonePermission();
await assistant.requestSpeechRecognitionPermission();- Requires iOS 13.0+
- SiriKit for iOS 10+ compatibility
- App Intents for iOS 16+ features
- Automatic Info.plist and entitlements configuration via config plugin
- Requires Android API 23+
- Google Assistant App Actions
- Built-in Intents (BIIs) support
- Automatic AndroidManifest.xml and shortcuts.xml configuration via config plugin
- Search - Voice-powered search queries
- Media - Playback control (play, pause, skip)
- Productivity - Tasks, notes, reminders
- Health - Workouts and fitness tracking
- Communication - Messages and calls
- Travel - Reservations and navigation
- Finance - Payments and transactions
- Commerce - Shopping and orders
{
"expo": {
"plugins": [
[
"expo-assistant",
{
"intents": ["media", "productivity"],
"enableBackgroundExecution": true,
"ios": {
"siriUsageDescription": "Control music with your voice",
"alternativeAppNames": ["My Music App"],
"requiresUnlock": false
},
"android": {
"appActionsTestUrl": "https://myapp.com/test-actions",
"slicesEnabled": true
}
}
]
]
}
}initialize(config?)- Initialize the assistantregisterIntent(intent)- Register a voice intentunregisterIntent(intentId)- Unregister an intentexecuteIntent(intentId, params)- Execute an intent programmaticallyrequestMicrophonePermission()- Request mic accessrequestSpeechRecognitionPermission()- Request speech recognitioncheckCapabilities()- Check platform capabilities
Fluent API for building voice intents:
const intent = VoiceIntentBuilder
.create<ParamsType>()
.withId('unique-id')
.withCategory(IntentCategory.MEDIA)
.requiredParameter('title', { type: ParameterType.STRING })
.optionalParameter('artist', { type: ParameterType.STRING })
.withHandler({ handle: async (params) => {...} })
.configureIOS(ios => ios.addSiriPhrase('Play music'))
.configureAndroid(android => android.withCapability('actions.intent.PLAY_MEDIA'))
.build();const playIntent = VoiceIntentBuilder
.create<{ mediaTitle: string }>()
.withId('play-media')
.withCategory(IntentCategory.MEDIA)
.requiredParameter('mediaTitle', { type: ParameterType.STRING })
.withHandler({
handle: async ({ mediaTitle }) => {
await mediaPlayer.play(mediaTitle);
return { success: true };
}
})
.withBackgroundExecution()
.build();const todoIntent = VoiceIntentBuilder
.create<{ action: 'add' | 'complete'; item: string }>()
.withId('manage-todo')
.withCategory(IntentCategory.PRODUCTIVITY)
.requiredParameter('action', {
type: ParameterType.ENUM,
choices: ['add', 'complete']
})
.requiredParameter('item', { type: ParameterType.STRING })
.withHandler({
handle: async ({ action, item }) => {
if (action === 'add') {
return await todoService.add(item);
}
return await todoService.complete(item);
}
})
.build();- Expo SDK 53+ (tested with SDK 54)
- React Native 0.74.0+
- TypeScript 4.5+
- Physical device for testing (simulators have limited voice support)
- PLUGIN.md — config plugin reference (what gets written to Info.plist / AndroidManifest / shortcuts.xml)
- example/ — runnable example app driving the public API
MIT © shottah