Graine AI

Test and release

How to prove in-app calls work before you ship — the test plan, the tools, what the App Store and Google Play ask for, and a go-live checklist.

Before you test

  • The microphone first. Ask for it before the first call — during onboarding, say — on both platforms, so that a call answered on the lock screen has it.
  • Real phones. CallKit and VoIP pushes do not run in the iOS Simulator. An Android emulator can ring, but the lock screen, Bluetooth and battery savers need a real phone — ideally two, from different manufacturers.
  • The right push host. A build run from Xcode gets a sandbox VoIP token, for api.sandbox.push.apple.com; TestFlight and App Store builds use api.push.apple.com.
  • Debug logs. On iOS and Android, GraineConfig(debug: true) logs the call's main steps — in Xcode's console under the ai.graine.inapp subsystem, in logcat under the tag Graine. Warnings and errors are logged always; audio, messages and variable values never. In React Native and Flutter, set debug: true in the calls options.
  • Call History. Each test call appears in the dashboard under the customer's name from identify(), with its transcript and outcome.

The test plan

Run every row on iOS and on Android, with the outcome your server receives checked against the last column.

#Do thisExpectOutcomes
1Tap your call buttonThe call screen opens and the agent speaks first, using your reasonended (local) when you hang up
2Ring with your app openiOS: the system's incoming call, as a banner. Android: the call screen opens straight awayanswered, then ended
3Ring with your app in the backgroundiOS: the system's incoming call. Android: a call notification with Answer and Declineanswered, then ended
4Ring with your app swiped awayIt still rings: iOS relaunches the app for the push, FCM wakes itanswered, then ended
5Ring a locked phoneiOS: the full-screen incoming call. Android: the full-screen ring, or a heads-up on Android 14+ without the full-screen permissionanswered, then ended
6Answer on the lock screen, lock again, open another appThe call goes on; on Android, the ongoing notification has Hang up
7DeclineThe ring stopsdeclined
8Let it ring outIt stops after the ring timeoutmissed
9Ring during a callThe call goes on, untouchedmissed for the new ring
10Let the agent end the callThe call screen closesended (remote)
11Use a Bluetooth headset, then switch it off mid-callThe call follows it, as a phone call does, and comes back to the phone when it goes
12Turn on airplane mode mid-callThe call ends; it does not reconnectended (failed)
13Deny the microphone, then place a callThe call does not connect. Android asks on the call screen firstfailed
14Deny the microphone when it is asked for after Answer (or deny it beforehand)The call does not connectanswered, then failed
15Android 13+: deny notifications, then ring with your app closedNothing ringsmissed
16Receive a phone call mid-call, then answer itiOS: the customer can end the agent's call and take the phone call. Android: the agent's call goes on hold while the phone rings, and ends when the customer answersended (local)
17Ring a device after reinstalling the appAPNs answers 410, FCM UNREGISTERED, and your server forgets the token

With WebRTC in the build (Calls over WebRTC), run rows 1, 2, 5 and 12 again, then these:

#Do thisExpectOutcomes
18Tap your call button, and answer a ringBoth connect as fast as before, the greeting not clipped; the call's transport is webrtcAs rows 1 and 2
19Set the agent's In-app call audio to Standard, restart the app, place a callIt connects; transport is websocket (Android: transportFallbackReason is dashboard). Set it back
20Place a call on a network that blocks WebRTC (some office Wi-Fi)It still connects, on the WebSocket, with no second ring; transportFallbackReason says why (often ice_timeout)As row 1
21Mid-call over WebRTC, walk out of Wi-Fi onto mobile dataThe call holds, or recovers within a few seconds; a network that stays down ends itended (failed) if it ends
22Android: a release build with R8Calls still use WebRTC

In React Native on Android with callScreen: "app", rows 13 and 14 differ: a refused microphone places and answers nothing, so there is no outcome — startCall returns null, the call's error says why, and a ring keeps ringing until it is declined or missed.

To send the pushes, use Send a test push. When something does not ring or connect, start from Troubleshooting.

The App Store

Every capability, background mode and manifest entry calls need, per platform, is on Permissions.

  • Capabilities. Push Notifications, and Background Modes with Audio, AirPlay, and Picture in Picture and Voice over IP. Calls do not work without both background modes.
  • Review notes. Say that the app offers voice calls with your assistant, and give the reviewer a way to place one: a test account and where the call button is. Describe how your agent calls customers, since the reviewer cannot receive your server's 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 for the system APIs it uses (it does no tracking), in the Swift package and in the Flutter and React Native pods alike; what your app collects — the call's audio among it — goes in your App Privacy details.
  • China. Apple does not allow CallKit in apps on the App Store in China: leave calls off there.

Google Play

  • Foreground service declaration. Apps targeting Android 14 or later declare each foreground service type they use in Play Console (App content → Foreground service permissions). The SDK's call service is of 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 a call doing so.
  • Full-screen intent. From Android 14, Google Play grants USE_FULL_SCREEN_INTENT by default only to apps whose main purpose is calling or alarms, and asks the others for a declaration in Play Console. Without it, calls still ring, as a heads-up notification. calls.callPermissions.fullScreen tells your app which it has, and calls.openCallSettings(GraineCallPermission.FULL_SCREEN) opens the setting for the customer.
  • Data safety. Declare the audio your app sends for the conversation.
  • Notifications and microphone. Ask during onboarding with requestCallPermissions() (the microphone, and POST_NOTIFICATIONS on Android 13+), so the first ring is not answered into a permission prompt.
  • The setup check. Before a release, read callSetupProblems on a test phone (or the log at configure): it should be empty, or name only what you chose (hosted_ringing_off when your own server sends the pushes).
  • An app that does not use calls can remove the call permissions and service from its manifest — see Android → Calls.

React Native and Flutter releases

Calls are native code. Adding the calls package, or upgrading it, needs a new store build: an over-the-air update cannot deliver it. So does turning WebRTC on: the build switch (GRAINE_WEBRTC) adds a native library.

Go-live checklist

The agent

  • The app's publishable key is in the build, and app:// is in the agent's Allowed domains.
  • The prompt handles both directions with {who_called} and {call_reason}, and reads well when a call has no reason.

iOS

  • Push Notifications, and Background Modes with Audio and Voice over IP.
  • NSMicrophoneUsageDescription in Info.plist.
  • enableVoIPPushes runs in didFinishLaunching, at every launch.
  • The APNs auth key is on your server, which uses the production host for TestFlight and App Store builds.

Android

  • Firebase is in the app (google-services.json, its Gradle plugin, the messaging library): the SDK's own messaging service does the rest. An app with its own messaging service calls handlePush before anything else.
  • Notifications and the microphone are asked for during onboarding.
  • Play Console declarations are made for the microphone foreground service and, if it applies to your app, the full-screen intent.

WebRTC (optional)

  • iOS: GraineInAppRTC at GraineInApp's version; Android: ai.graine:inapp-android-rtc at the SDK's version; React Native and Flutter: the build switch on for both platforms.
  • WebRTC (BSD 3-Clause) is listed in your app's open-source licences.
  • The agent's In-app call audio is WebRTC (recommended), and rows 18–22 pass.

Your server

  • Tokens are saved at every launch and deleted on 410 and UNREGISTERED.
  • A ring timer turns unanswered calls into missed, and outcome reports are safe to repeat.
  • Your calling policy is in place: consent, hours, how often.

Tested

  • The test plan above, on at least one iPhone and two Android phones from different manufacturers.
  • Debug logging is off in the release build.

On this page