Graine AI

Permissions

Every OS permission, capability, background mode, manifest entry and store declaration each feature needs on React Native (with Expo), iOS, Android and Flutter — who asks and when, the words to use, what a refusal does, and what App Store and Google Play review look for.

One rule covers every SDK: the SDK never asks on its own at launch. It asks for the microphone when a call starts without it; for calls, one call — requestCallPermissions() — asks for everything the system's dialogs can still ask for, at a moment your app chooses. What a feature needs at build time your app declares (the calls packages' Android manifests merge it for you), and the SDK's setup check says, in plain words, anything that is still missing.

At a glance

FeatureiOSAndroid
ChatNothingNothing beyond INTERNET
Voice in the barNSMicrophoneUsageDescriptionRECORD_AUDIO, MODIFY_AUDIO_SETTINGS
Calls the customer placesThe microphone, plus Push Notifications and the Audio and Voice over IP background modesThe microphone and a microphone foreground service. Without notifications allowed the call still runs; its ongoing notification does not show
Calls that ring the customerThe above. No notification permission: VoIP pushes and CallKit do not need oneThe above, plus POST_NOTIFICATIONS (Android 13+) granted by the customer, and the full-screen-intent permission (Android 14+) for a full-screen ring
Hosted ringingNothing more than calls that ring; the SDK registers the VoIP token itselfNothing more than calls that ring; forward Firebase's token to the SDK
Bluetooth and headsetsNothingNothing — routing a call needs no Bluetooth permission

Who adds what, per SDK:

DeclarationsAsked at run time
React Native — @graineai/inapp-react-nativeYou add the microphone: RECORD_AUDIO and MODIFY_AUDIO_SETTINGS, NSMicrophoneUsageDescription. The package carries no manifestAndroid: the SDK asks for the microphone when a call starts. iOS: the system asks on first use
React Native — @graineai/inapp-react-native-callsIts Android manifest merges everything a call needs. On iOS its Expo config plugin adds the capabilities and the usage string; without Expo you add themThe SDK asks for the microphone when a call starts; requestCallPermissions() asks for the microphone and notifications when you choose
Android — ai.graine:inapp-androidThe library's manifest merges everything, voice and callsThe SDK asks for the microphone when a call starts; calls.requestCallPermissions() asks for the microphone and notifications when you choose
iOS — GraineInAppYou add the usage string and, for calls, the capabilitiesThe SDK asks for the microphone when a call starts, or at calls.requestCallPermissions()
Flutter — graine_inappFrom 0.6.1 its Android manifest merges everything; on iOS you add the usage string and, for calls, the capabilitiesFor voice in the bar, your app asks, through requestMicrophone. For calls, the SDK asks when a call starts; requestCallPermissions() asks for the microphone and notifications when you choose
Kotlin Multiplatform — ai.graine:inapp-kmpAs Android and iOSAs Android and iOS: Graine.calls.requestCallPermissions()

How it works

AT BUILD TIMEYour app declaresthe microphonefor calls: capabilities,background modes,manifest entriesDURING ONBOARDINGYour app asksthe microphonenotifications —Android 13+, for callsat a moment you chooseWHEN A CALL STARTSThe SDK asksthe microphone, onlyif it is still missing(Flutter's bar: yourrequestMicrophone)WHEN A CALL RINGSThe system shows itiOS: CallKitAndroid: a notification,full screen when lockedif allowedA refusal is reported to your app (onMicDenied, the call's outcome) and never asked again by the SDK

Ask at onboarding and the SDK never needs to: it finds the microphone granted and asks nothing.

Voice in the bar

The microphone, on every platform.

DeclareWho asks, and whenIf it is refused
React NativeRECORD_AUDIO, MODIFY_AUDIO_SETTINGS; NSMicrophoneUsageDescriptionAndroid: the SDK, when a call starts — including the moment the engine is ready, for a launcher that is active with autoStart. iOS: the system, the first time the call uses the microphoneNo call. onMicDenied("NotAllowedError"), api.micDenied, and GraineVoiceBar shows Needs microphone
AndroidNothingThe SDK, at voice.start(). Forward your activity's onRequestPermissionsResult to voice.onRequestPermissionsResultNo call. onMicDenied; the bar shows Needs microphone, and comes back by itself when the customer returns from Settings with it allowed
iOSNSMicrophoneUsageDescriptionThe SDK, at voice.start()No call. onMicDenied; the bar shows Needs microphone until the next call starts
FlutterRECORD_AUDIO; NSMicrophoneUsageDescriptionYour app, in requestMicrophone, which the SDK calls before every call. Left out, the SDK assumes you askedrequestMicrophone returned false: no call, onMicDenied("NotAllowedError"), micDenied
  • NotAllowedError is a refusal; NotFoundError is a device with no usable microphone.
  • The SDK never asks twice by itself. A later call asks again only where the system allows: iOS shows its dialog once, then only Settings can change it; Android may show it again, until the customer has refused it for good.
  • Nothing falls back. A refused microphone never moves the call to another transport.
  • iPhone's ringer switch. React Native leaves the audio session to your app: while a call is live set playsInSilentModeIOS: true and allowsRecordingIOS: true, or the agent is silent whenever the ringer is off. The native iOS SDK sets its own session.

Calls

Native calls — the customer calls the agent, or the agent's call rings the customer — need more, because they run with the screen off and ring on the lock screen.

In Xcode, the app target's Signing & Capabilities:

AddWhy
Push NotificationsVoIP pushes reach the app
Background Modes → Audio, AirPlay, and Picture in PictureThe call's audio keeps going with the screen off
Background Modes → Voice over IPiOS wakes a closed app for a VoIP push
NSMicrophoneUsageDescription in Info.plistThe microphone
NSCameraUsageDescription in Info.plist — with the WebRTC moduleNothing at run time: calls are audio only. App Store processing requires it because WebRTC contains camera code (ITMS-90683)

Nothing else: no notification permission (VoIP pushes and CallKit need none), no Bluetooth usage description, no remote-notification background mode.

The microphone is asked by the SDK before a call the customer places is placed. For a call that rings, the customer answers first: the SDK reports answered, the system asks while the call connects, and a refusal — or no answer within 60 seconds — ends it failed. Ask during onboarding with calls.requestCallPermissions() so a call answered on the lock screen already has it.

The setup check reads Info.plist and the build's signing at calls.configure and names whichever of the four above is missing (microphone_usage_missing, background_audio_missing, background_voip_missing, push_entitlement_missing).

Asking, reading and opening Settings

Every SDK has the same four, in its own idiom:

Ask (when you choose)Read nowOpen SettingsWhat is still missing
iOSawait calls.requestCallPermissions()calls.callPermissionscalls.openCallSettings(.microphone)calls.callSetupProblems
Androidcalls.requestCallPermissions(activity) { p -> }calls.callPermissionscalls.openCallSettings(GraineCallPermission.FULL_SCREEN)calls.callSetupProblems
React Nativeawait GraineCalls.requestCallPermissions()await GraineCalls.callPermissions()GraineCalls.openCallSettings("fullScreen")await GraineCalls.callSetupProblems()
Flutterawait GraineCalls.instance.requestCallPermissions()await GraineCalls.instance.callPermissions()GraineCalls.instance.openCallSettings(GraineCallPermission.fullScreen)await GraineCalls.instance.callSetupProblems()
Kotlin MultiplatformGraine.calls.requestCallPermissions()Graine.calls.callPermissionsGraine.calls.openCallSettings(GraineCallPermission.FULL_SCREEN)Graine.calls.callSetupProblems

Three permissions, each with one of four statuses:

PermissioniOSAndroid
microphoneAsked once; refused, only Settings can change itAsked; refused for good, only Settings can change it
notificationsnotNeeded — CallKit rings without themAndroid 13+ asks; before 13, on unless turned off in Settings
fullScreennotNeeded — CallKit draws its own screenAndroid 14+: Settings only; before, granted with the app

granted; denied — not yet, asking may still change it; blocked — only Settings can change it; notNeeded — this phone needs no such permission. allGranted is true when each one is granted or not needed. Asking never leaves your app: the full-screen ring, which has no dialog, is opened with openCallSettings. Read the permissions again when your app returns to the foreground — the customer may have changed Settings.

What is still missing

At calls.configure the SDK checks the app's setup for calls and logs each problem once, as a warning with the exact fix — in Xcode's console (subsystem ai.graine.inapp) and logcat, and also in Metro's console (React Native) or the flutter run console in a development build. callSetupProblems returns the same list, each a stable code and a message:

CodePlatformWhat it means, and the fix
microphone_usage_missingiOSNo NSMicrophoneUsageDescription: iOS closes the app when a call asks for the microphone. Add it
background_audio_missingiOSNo audio background mode: a call goes silent when the screen locks
background_voip_missingiOSNo voip background mode: iOS does not wake the app for a ring
push_entitlement_missingiOSSigned without Push Notifications — read from development and ad hoc builds; App Store builds embed nothing to read
voip_pushes_not_enablediOSenableVoIPPushes not called 15 seconds after configure: calls can't ring
no_voip_tokeniOSNo VoIP token 15 seconds after enableVoIPPushes (never reported on the simulator, which gets none)
manifest_entry_missingAndroidThe merged manifest lost a call entry — look for tools:node="remove"
no_push_tokenAndroidNo Firebase token reached the SDK 15 seconds after configure: Firebase is missing or not set up in the app, its automatic token creation is off, or Firebase could not give one — the message says which. An app with its own messaging service hands tokens over (onNewToken, registerPushToken)
notifications_not_allowedAndroidA call can't ring while the app is closed: ask, or offer Settings if they were turned off
full_screen_not_allowedAndroidA locked phone rings with a heads-up notification: offer openCallSettings(fullScreen)
microphone_blockedBothOnly Settings can let calls hear the customer: offer openCallSettings(microphone)
hosted_ringing_offBothGraine can't ring this phone: no push credentials uploaded for this platform (unless your own server sends the pushes)

A problem is never an exception: calls still work wherever they can.

Not using calls

On Android 14 and later, Google Play asks apps that declare USE_FULL_SCREEN_INTENT or FOREGROUND_SERVICE_MICROPHONE to explain why. An app that never places a call can remove them, and the call's service, in its own manifest — see Android → Calls. POST_NOTIFICATIONS and FOREGROUND_SERVICE can go too; an app that keeps calls must keep all of them.

Hosted ringing

No new permission. A call Graine rings needs exactly what any call that rings needs — the iOS capabilities, and on Android the notification and full-screen permissions above. What changes is who holds the tokens: on iOS, enableVoIPPushes at launch registers the VoIP token with Graine; on Android, with Firebase in the app, the SDK registers Firebase's token itself (an app with its own messaging service hands it over in onNewToken). Hosted ringing

Bluetooth and headsets

No permission on any platform. A call goes to a Bluetooth headset or car kit when one is connected, else a wired headset, else the earpiece, and follows when a headset comes or goes. On Android that routing runs on MODIFY_AUDIO_SETTINGS, which every SDK already declares — no BLUETOOTH_CONNECT. On iOS the call's audio session allows Bluetooth hands-free, with no Bluetooth usage description. Voice in the bar follows the phone's current audio route.

Asking at the right moment

Ask during onboarding, at a moment that explains itself ("Talk to Asha"), and before the first call — then the SDK never prompts in the middle of one. With calls, requestCallPermissions() is that one line on every SDK (the table above); the examples below ask without the calls API, for voice in the bar.

import { PermissionsAndroid, Platform } from "react-native";
import { requestMicrophonePermission } from "@graineai/inapp-react-native";
 
export async function askForAgentPermissions() {
  // Android shows your words; iOS resolves true and asks on first use.
  await requestMicrophonePermission({
    title: "Talk to Asha",
    message: "Allow the microphone so you can talk to the assistant.",
    buttonPositive: "Allow",
  });
  if (Platform.OS === "android" && Platform.Version >= 33) {
    await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS); // calls only
  }
}

To show iOS's prompt earlier than the first call, use the permission library your app already has.

The words to use

WhereSuggested text
iOS — NSMicrophoneUsageDescriptionUsed to talk to the in-app assistant. — or, with calls, Lets you talk to Asha in the app and on calls. Say what the customer gets, not what the app does
Android — the microphone, before you askAllow the microphone so you can talk to the assistant. — the words React Native's SDK uses when it asks
Android — notifications, before you askAllow notifications so calls from Asha can ring.
Android — full screen, before you open the settingAllow full-screen notifications so a call from Asha fills the screen when your phone is locked.
After a refusalThe microphone is off for Acme. Turn it on in Settings to talk to Asha. — with a button to Settings

Store review

The App Store

  • Voice over IP is for calls only. iOS requires every VoIP push to be reported to CallKit at once and stops delivering them to apps that do not. The SDK reports every one; send VoIP pushes only for calls.
  • Review notes. Say the app offers voice calls with your assistant, give the reviewer a test account and where the call button is, and describe how your agent's calls ring — the reviewer cannot receive your push.
  • Privacy. The microphone purpose string is required — and, with the WebRTC module, the camera one, NSCameraUsageDescription — WebRTC contains camera code, and App Store processing refuses a build that references the camera without it (ITMS-90683: the build never reaches TestFlight). Calls are audio only; say so, for example Calls are audio only. The camera is never turned on. The SDK ships a privacy manifest declaring the required-reason APIs it calls — system boot time, for timing, and UserDefaults — and no tracking. What your app collects through the agent, the customer's voice included, goes in your App Privacy details.
  • China. Apple does not allow CallKit in apps on the App Store in China: leave calls off there.
  • Self-contained apps (guideline 2.5.2). The SDK downloads no code. What the agent sends while it runs is content and settings — its words, its voice, the cards and choices it shows, and your Garage design — drawn by code already in your app. Apple does reject builds that carry development tools able to load code from a URL: leave them out of store builds. With Expo, remove the development client (expo-dev-client) from the build you submit — it compiles into release builds even though it only runs in development. Over-the-air JavaScript updates are your app's choice; keep them to fixes within the features Apple reviewed.
  • A note for the reviewer. For example: "The app includes an AI assistant you can chat and talk with. Its replies, its voice and the appearance of the chat and call screens are delivered as content and settings from our server and drawn by the app's own code; no executable code is downloaded. Calls use CallKit and VoIP push only for real incoming calls."

Google Play

  • Foreground service. Apps targeting Android 14 or later declare each foreground service type in Play Console (App content → Foreground service permissions). The call's service is type microphone: describe it as two-way voice calls with your assistant that keep going when the screen locks, and link a short video of one.
  • Full-screen intent. Granted by default only to apps whose main purpose is calling or alarms; other apps declare it in Play Console, or go without — the ring is then a heads-up notification.
  • Data safety. Declare the audio your app sends for the conversation.
  • Executable code. Google Play forbids apps from downloading code (dex files or native libraries) outside Play. The SDK downloads none; the agent's replies, components and your design arrive as content.

Troubleshooting

What you seeWhy
Anything below, with callsRead callSetupProblems (or the log at configure): it names what is missing and the fix
Needs microphone, and no dialog ever appearedThe permission is not declared — RECORD_AUDIO in the manifest (React Native and Flutter), or the usage string on iOS — or the customer refused it before
iOS: the app closes as a call starts, or the microphone is never offeredNSMicrophoneUsageDescription is missing: iOS stops an app that asks for the microphone without one
Android: a granted call stays on Connecting…Your activity does not forward onRequestPermissionsResult to the SDK
Flutter: the microphone is never grantedrequestMicrophone is not set, or the manifest or Info.plist entry is missing
Android 13+: calls never ring with the app closedNotifications are not allowed. Ask during onboarding
Android 14+: a heads-up instead of a full-screen ringNo full-screen-intent permission. It still rings; offer the setting with openCallSettings(fullScreen)
iOS: a call answered on the lock screen is silentBackground Modes must include Audio and Voice over IP

Where next

On this page