Put your bot inside your own iOS and Android apps, native or built with Flutter or React Native, with a native chat screen, signed-in users and push notifications.
The mobile SDKs show your flow in a native chat screen inside your app. Visitors get the same conversation they would on your website: the flow's messages and choices, its ways in, photos and files they send, a rating at the end when you ask for one, and a person taking over when the flow hands off. When an agent replies while the app is closed, the visitor gets a notification that opens the chat.
There are SDKs for Android and iOS, and packages for Flutter and React Native apps that show the same native chat screen.
The chat takes its colours, title and greeting from the deployment's widget settings, in light and dark to match the phone.
Mobile app deployments need the Pro or Business plan. If your plan later drops them, existing ones stay set up but stop answering, and start again once the plan includes them.
The Android SDK supports Android 7.0 (API 24) and later. Add it to your app module's dependencies:
dependencies {
implementation("com.universalchatbot:universalchat:1.0.0")
}
Configure it once when your app starts, then open the chat from anywhere:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
UniversalChat.configure(this, serverUrl = "SERVER_URL", deploymentId = "DEPLOYMENT_ID")
}
}
// From a button
UniversalChat.present(activity)
// Facts about the visitor, for flows and agents
UniversalChat.setAttributes(mapOf("screen" to "orders", "app_version" to BuildConfig.VERSION_NAME))
When a user signs in to your app, fetch a token for them from your server and pass it on. Sign them out with your own sign-out:
lifecycleScope.launch {
UniversalChat.login(tokenFromYourServer)
.onFailure { Log.w("Chat", "Sign-in refused", it) }
}
UniversalChat.logout()
The iOS SDK supports iOS 16 and later. In Xcode, choose File → Add Package Dependencies, enter https://github.com/radioactive-labs/universalchat-ios, and add the UniversalChat library to your app target. Then configure it at launch:
import UniversalChat
// In application(_:didFinishLaunchingWithOptions:)
UniversalChat.configure(serverURL: URL(string: "SERVER_URL")!, deploymentID: "DEPLOYMENT_ID")
// From a view controller
UniversalChat.present(from: self)
// Or inside SwiftUI
.sheet(isPresented: $showChat) { UniversalChatView { showChat = false } }
// Facts about the visitor, for flows and agents
UniversalChat.setAttributes(["screen": "orders", "app_version": "4.2.0"])
// Signing in and out
try await UniversalChat.login(userToken: tokenFromYourServer)
UniversalChat.logout()
The universal_chat plugin shows the same native chat screen in a Flutter app, on Android 7.0 and iOS 16 or later. Add it to your
app's pubspec.yaml. On iOS it is a Swift package, so turn on Flutter's Swift Package Manager support with
flutter config --enable-swift-package-manager.
dependencies:
universal_chat: ^1.0.0
import 'package:universal_chat/universal_chat.dart';
await UniversalChat.configure(serverUrl: 'SERVER_URL', deploymentId: 'DEPLOYMENT_ID');
await UniversalChat.present();
await UniversalChat.setAttributes({'screen': 'orders'});
await UniversalChat.login(tokenFromYourServer);
The @universalchatbot/react-native module shows the same native chat screen in a React Native app, from React Native 0.75, on
Android 7.0 and iOS 16 or later. Install it, then rebuild the app.
npm install @universalchatbot/react-native
npx pod-install
With Expo, add it to your app's plugins in app.json and run npx expo prebuild:
{ "expo": { "plugins": ["@universalchatbot/react-native"] } }
import UniversalChat from '@universalchatbot/react-native'
await UniversalChat.configure({ serverUrl: 'SERVER_URL', deploymentId: 'DEPLOYMENT_ID' })
await UniversalChat.present()
await UniversalChat.setAttributes({ screen: 'orders' })
await UniversalChat.login(tokenFromYourServer)
Flutter and React Native apps pass push on the same way as native ones: hand over the Firebase token with setPushToken, messages
that arrive while the app is open with handlePush, and tapped notifications with handleNotificationTap. On iOS,
shouldPresentNotification says whether to show a notification while the app is open: not when the chat it is about is on screen.
Notifications go through your app's own Firebase project, on both Android and iOS. In the deployment's App setup, upload a service account key for that project: in the Firebase console, open Project settings, then Service accounts, and choose Generate new private key. For iOS, Firebase must also have your APNs key, as Firebase's own setup describes.
Then hand the SDK your Firebase token, and pass on the notifications it sends:
// Android, in your FirebaseMessagingService
override fun onNewToken(token: String) = UniversalChat.setPushToken(token)
override fun onMessageReceived(message: RemoteMessage) {
if (UniversalChat.handlePush(this, message.data, message.notification?.title, message.notification?.body)) return
// your own notifications
}
// And in your launcher activity, for a notification tapped while the app was closed
UniversalChat.handleNotificationTap(this, intent)
// iOS, in your MessagingDelegate
func messaging(_ messaging: Messaging, didReceiveRegistrationToken token: String?) {
if let token { UniversalChat.setPushToken(token) }
}
// In your UNUserNotificationCenterDelegate
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
UniversalChat.shouldPresentNotification(notification.request.content.userInfo) ? [.banner, .sound] : []
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
UniversalChat.handleNotification(response.notification.request.content.userInfo, presentingFrom: rootViewController)
}
A notification is sent when an agent replies, to every device the visitor has the app on. Nothing is shown while the chat is already open, since the reply appears there. Signing out stops that device getting the user's notifications.
Tapping one opens the conversation it was about. If that conversation has ended since, a signed-in user sees it in their past conversations; anyone else gets the chat.
A flow or an agent can send a link to a screen of your app, such as myapp://orders/1001. Links open as they would anywhere else on
the phone unless your app takes them first: return true for the ones it handled, and the chat closes so the visitor lands on that screen.
// Android
UniversalChat.linkHandler = { url ->
if (url.startsWith("myapp://")) { router.open(url); true } else false
}
// iOS
UniversalChat.linkHandler = { url in
guard url.scheme == "myapp" else { return false }
router.open(url)
return true
}
In Flutter and React Native, name the links your app takes by how they start, and the handler gets each one the visitor taps:
// Flutter
UniversalChat.setLinkHandler(prefixes: ['myapp://'], onLink: (url) => router.go(url));
// React Native
UniversalChat.setLinkHandler(['myapp://'], (url) => navigation.navigate(routeFor(url)))
To open the chat from a link of your own, such as myapp://support in an email, route that link to UniversalChat.present
the way your app routes any other.
When someone is signed in to your app, you can tell the chat who they are. Their conversation then follows them to any device they sign in on, agents see the details you send as verified, and the bot never asks them for details you already gave it.
The chat opens on the visitor's most recent conversation. A signed-in user can also look back at their earlier ones: the Past conversations button in the chat's header lists them newest first, and opens any of them to read. Visitors who are not signed in only see their most recent conversation, since a shared computer or tablet would otherwise show one person's chats to the next.
Your server proves who the user is by signing a token for them with the deployment's identity secret, which builders, owners, managers and admins can copy from the deployment's settings. The token is a standard JWT signed with HS256 and carries:
sub requiredexp recommendedname, email, phoneattributes{"plan": "pro"}. See below.# Ruby, on your server
require "jwt"
token = JWT.encode(
{sub: user.id.to_s, name: user.name, email: user.email,
attributes: {plan: user.plan, account_id: user.account_id},
exp: 1.hour.from_now.to_i},
ENV.fetch("CHAT_IDENTITY_SECRET"), "HS256"
)
// Node, on your server
const jwt = require("jsonwebtoken");
const token = jwt.sign(
{ sub: String(user.id), name: user.name, email: user.email, attributes: { plan: user.plan } },
process.env.CHAT_IDENTITY_SECRET,
{ algorithm: "HS256", expiresIn: "1h" }
);
Keep the secret on your server
Anyone holding the identity secret can sign in to the chat as any of your users, so never put it in your app. If it leaks, replace it from the deployment's settings: everyone signed in under the old secret is signed out of the chat straight away, and the tokens your server signs with the new one sign them back in.
A token that does not check out is refused, and the chat carries on as an anonymous visitor.
Attributes are facts about the visitor that your flows can branch on and agents can read beside the conversation, such as their plan, account id or the
screen they opened the chat from. Values are text, numbers or true and false, under names made of letters, digits, _, - and ..
Up to 25 are kept, and text is cut at 255 characters.
There are two ways to pass them:
attributes claim. Your server vouched for these, so a flow can rely on them.When both carry the same name, the signed value wins. In a flow, read them in any text or condition:
{{@visitor.attributes.plan}} any attribute
{{@visitor.verified_attributes.plan}} signed attributes only
{{@visitor.user_id}} your id for a signed-in user
{{@visitor.signed_in}} true when the user is signed in
In the inbox, attributes appear in the conversation's details panel, with a check beside the ones your server signed.