Native SDKs
Lane.Chat ships native SDKs for Android (Kotlin), iOS (Swift), and Flutter (Dart) — all at version 0.3.0, with aligned API names. Each presents the Lane.Chat conversation inside your app, collects a privacy-aware device fingerprint, forwards push tokens, and shares one visitor identity across native and web.
The SDKs manage their own connection — there is nothing to host and no endpoint to configure.
What the SDKs do
- Present the Lane.Chat conversation UI inside your app — link handling, share, and close are built in.
- Collect basic device characteristics (device id, model, OS, locale) — no ad IDs, no contacts, no location.
- Forward APNs/FCM tokens so Lane.Chat's servers send pushes; your signing key never ships in the app.
- Bind
identify()traits and a user id so one device maps to one visitor across native and web chat. - Surface replies while the chat is closed: system notifications, an in-app banner, or your own UI via event listeners (Android), plus event streams and push-tap routing on every platform.
| Android | iOS | Flutter | |
|---|---|---|---|
| Language | Kotlin | Swift | Dart |
| Install | Maven Central chat.lane:sdk | Swift Package Manager | pub.dev lanechat_flutter |
| Minimum | minSdk 24, Kotlin 1.9+, Java 17 | iOS 15, Swift 5.9, Xcode 15 | Flutter 3.24, Dart 3.5 |
| Push transport | FCM | APNs | FCM (both platforms, via your own firebase_messaging) |
Android
Install
dependencies {
implementation("chat.lane:sdk:0.3.0")
}Bootstrap
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
LaneChat.initialize(this, appId = "YOUR_APP_KEY")
}
}For link policy, WebView tuning, or the open animation, pass a full LaneChatConfig instead:
LaneChat.initialize(this, LaneChatConfig(
appId = "YOUR_APP_KEY",
linkPolicy = LinkPolicy(httpMode = HttpMode.IN_APP), // or EXTERNAL
chatOpenAnimation = ChatOpenAnimation.SLIDE_UP,
debug = BuildConfig.DEBUG,
))LaneChatConfig fields: appId (required), endpoint, widgetBaseUrl, linkPolicy, webView, debug, requestTimeoutMs, chatOpenAnimation.
Identify and open chat
LaneChat.identify(
userId = "alice_123",
traits = mapOf("plan" to "pro", "email" to "[email protected]", "order_id" to "X-42")
)
LaneChat.showChat(context) // agents see plan / email / order_id alongside the chatPrefer embedding? Use LaneChat.chatFragment() inside your own nav host.
Push (FCM)
Register the bundled messaging service in AndroidManifest.xml:
<service
android:name="chat.lane.sdk.LaneChatMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>It forwards new FCM tokens (LaneChat.setDeviceToken) and renders Lane.Chat pushes. If your app already has its own FirebaseMessagingService, keep it and delegate instead — isLaneChatPush(data) tells you whether a payload is Lane.Chat's, and handlePush(context, data) renders it and returns true when it was:
override fun onNewToken(token: String) = LaneChat.setDeviceToken(token)
override fun onMessageReceived(message: RemoteMessage) {
if (LaneChat.handlePush(this, message.data)) return // was ours, rendered
// ... your own push handling
}Android 13+ requires the runtime POST_NOTIFICATIONS permission. The SDK declares it but never prompts on its own — prompt timing is a UX decision only your app can make. Call this wherever fits your onboarding:
val granted = LaneChat.ensureNotificationPermission(activity)
// false = the system dialog was just shown; observe the result in
// onRequestPermissionsResult keyed on LaneChat.NOTIFICATION_PERMISSION_REQUEST_CODEBackground listening
By default the SDK only reacts to chat activity while the chat UI is open. setBackgroundListening adds a native presence WebSocket that stays connected while your app is in the foreground and the chat is closed, so a reply surfaces without the user having chat open:
LaneChat.setBackgroundListening(true, NoticeStyle.SYSTEM_NOTIFICATION)Off by default — no socket, no battery or network use until you call it. The NoticeStyle argument controls how the message is surfaced:
| Style | Behavior |
|---|---|
SYSTEM_NOTIFICATION (default) | A regular system notification; deduplicated against an FCM push for the same message. |
IN_APP_BANNER | A lightweight banner the SDK draws over your current Activity. Auto-dismisses after ~5 s; tap opens chat directly into the conversation. |
NONE | Nothing rendered — build your own UI from LaneChatEventListener callbacks. |
The two layers are independent and commonly both on: presence covers "app open, chat closed" instantly; FCM push covers backgrounded and killed apps.
Events and links
LaneChat.addListener(object : LaneChatEventListener {
override fun onUnreadCountChanged(count: Int) { updateBadge(count) }
override fun onMessageReceived(type: String, excerpt: String, isAi: Boolean) { }
// Also: onSessionLoaded, onChatOpened, onChatClosed, onMessageSent
})Callbacks arrive on the main thread; excerpt is a preview, never the full message body.
Links tapped inside the chat route in three tiers: your setLinkHandler { url, source -> Boolean } gets first refusal (return true to consume — in-app routing, deeplinks); non-http(s) schemes go to the OS; and http(s) opens per LinkPolicy.httpMode — IN_APP (Chrome Custom Tabs, default) or EXTERNAL (system browser). source is one of message, rich_button, kb_article, kb_citation, tg_binding, powered_by, navigation, home_link.
Chat open animation
ChatOpenAnimation controls how the chat screen animates in and out: SLIDE_UP (default), FADE, SYSTEM (platform default transition), or NONE. Set it in LaneChatConfig, or change it at runtime — it takes effect on the next open:
LaneChat.setChatOpenAnimation(ChatOpenAnimation.FADE)Android public API
| Method | Purpose |
|---|---|
LaneChat.initialize(context, config) | Bootstrap with a full LaneChatConfig. |
LaneChat.initialize(context, appId, endpoint?) | Convenience overload. |
LaneChat.identify(userId, traits?) | Attach a logged-in user (persisted across restarts). |
LaneChat.setSessionData(data) | Merge context into the current conversation only. See Two levels of visitor data. |
LaneChat.showChat(context) | Launch chat as a full-screen Activity. |
LaneChat.chatFragment() | Embed chat as a Fragment. |
LaneChat.setDeviceToken(token) | Forward an FCM token. |
LaneChat.isLaneChatPush(data) / LaneChat.handlePush(context, data) | Push triage for apps with their own messaging service. |
LaneChat.setPushEnabled(enabled) | Opt this device out of / back into push (unregisters or re-registers the current token server-side). |
LaneChat.ensureNotificationPermission(activity) | Android 13+: request POST_NOTIFICATIONS if not yet granted. |
LaneChat.setBackgroundListening(enabled, notice?) | Foreground presence socket + notice style. |
LaneChat.setChatOpenAnimation(style) | Change the open/close animation at runtime. |
LaneChat.preload() | Network warm-up (DNS/TLS/CDN) for the widget host ahead of the first open. |
LaneChat.track(event, properties?) | Custom analytics event. |
LaneChat.reset() | Clear identity and session on logout; restarts anonymous. |
LaneChat.addListener(listener) / removeListener(listener) | Chat lifecycle callbacks. |
LaneChat.setLinkHandler(handler) | Intercept links clicked inside the chat. |
LaneChat.onWebViewCreated | Raw WebView escape hatch, runs after SDK settings apply. |
All methods are thread-safe; network I/O runs on a background coroutine scope and calls return immediately. identify, setSessionData, track, and push registration wait for the session to exist instead of being dropped, so they are safe to call right after initialize.
iOS
Install (Swift Package Manager)
In Xcode: File → Add Package Dependencies…, add the Lane.Chat iOS SDK package, and select the LaneChatSDK product. Requires iOS 15+, Swift 5.9, Xcode 15. The SDK has zero third-party dependencies — pure Apple frameworks.
Bootstrap
import LaneChatSDK
// In AppDelegate.didFinishLaunchingWithOptions:
LaneChat.initialize(appId: "YOUR_APP_KEY")
// Forward APNs tokens:
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
LaneChat.setDeviceToken(deviceToken)
}For link policy or WebView tuning, pass a full config:
LaneChat.initialize(LaneChatConfig(
appId: "YOUR_APP_KEY",
linkPolicy: LaneChatLinkPolicy(httpMode: .inApp), // or .external
debug: true
))LaneChatConfig fields: appId (required), endpoint, widgetBaseUrl, linkPolicy, debug, webView.
Identify and open chat
LaneChat.identify(userId: currentUser.id, traits: [
"plan": currentUser.plan,
"email": currentUser.email,
"order_id": "X-42"
])
LaneChat.showChat(from: self) // agents see the traits alongside the chatPush (APNs)
Enable Push Notifications for your App ID and add the
remote-notificationbackground mode.Request authorization and register (helper included):
swiftPushRegistration.requestAuthorization { granted, _ in // iOS calls didRegisterForRemoteNotificationsWithDeviceToken on success }Forward the token with
LaneChat.setDeviceToken(_:)(above).
Route a push tap back into the conversation — Lane.Chat pushes are tagged lane_chat and may carry a deep link:
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
if let root = UIApplication.shared.connectedScenes
.compactMap({ ($0 as? UIWindowScene)?.keyWindow?.rootViewController })
.first {
PushRegistration.handlePush(userInfo, from: root) // presents chat + routes the deep link
}
completionHandler()
}iOS public API
| Method | Purpose |
|---|---|
LaneChat.initialize(appId:) / LaneChat.initialize(_ config:) | Bootstrap. Call once at launch. |
LaneChat.identify(userId:traits:) | Bind an end-user. Waits for the session — safe right after initialize. |
LaneChat.setSessionData(_:) | Merge context into the current conversation only. See Two levels of visitor data. |
LaneChat.showChat(from:) | Present the chat full-screen (main thread). |
LaneChat.chatViewController() | Get a UIViewController for custom embedding. |
LaneChat.setDeviceToken(_:) | Forward the raw APNs token (Data). |
LaneChat.setPushEnabled(_:) | Opt this device out of / back into push. |
LaneChat.track(_:properties:) | Custom events. |
LaneChat.reset() | Wipe identity on logout; restarts an anonymous session. |
LaneChat.addListener(_:) / removeListener(_:) | Multicast LaneChatDelegate events (laneChatOpened, laneChatClosed, laneChatMessageReceived, laneChatUnreadCountChanged, …). Weakly held, main-thread. |
LaneChat.setLinkHandler(_:) | Intercept links before the SDK opens them; return true to consume. |
LaneChat.webViewConfigurator / LaneChat.onWebViewCreated | Raw WebView escape hatches. |
PushRegistration.requestAuthorization(options:completion:) | Notification auth + register helper. |
PushRegistration.isLaneChatPush(_:) / .deepLinkURL(from:) / .handlePush(_:from:) | Push triage and tap handling. |
The SDK is thread-safe; UI calls (showChat) must run on the main thread. Non-http(s) link schemes go to the system; http(s) opens per linkPolicy.httpMode — .inApp (SFSafariViewController, default) or .external (system browser).
Flutter
Lane.Chat ships an official Flutter SDK — pure Dart on top of webview_flutter, no custom platform code to vendor, with the same API names as the Android and iOS SDKs.
Install
flutter pub add lanechat_flutterBootstrap, identify, open chat
import 'package:lanechat_flutter/lanechat_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await LaneChat.initialize('YOUR_APP_KEY');
runApp(const MyApp());
}
// After login:
await LaneChat.identify('alice_123', traits: {'plan': 'pro', 'email': '[email protected]'});
// From any button:
LaneChat.showChat(context);showChat pushes a full-screen LaneChatChatPage. To place chat inside your own layout, embed LaneChatView and decide what "close" means:
LaneChatView(onCloseRequested: () => Navigator.of(context).maybePop())Events
Everything the widget reports is available as typed streams:
LaneChat.onUnreadChanged.listen((count) => setBadge(count));
LaneChat.onMessageReceived.listen((m) => debugPrint('${m.type}: ${m.excerpt}'));
// Also: LaneChat.events, onSessionLoaded, onChatOpened, onChatClosed, onMessageSentPush (FCM)
The SDK does not depend on firebase_messaging — bring your own and forward tokens:
final messaging = FirebaseMessaging.instance;
await messaging.requestPermission();
final token = await messaging.getToken();
if (token != null) await LaneChat.setDeviceToken(token);
messaging.onTokenRefresh.listen(LaneChat.setDeviceToken);iOS + firebase_messaging: use getToken(), never the raw APNs token
On iOS, firebase_messaging exposes both an APNs device token (getAPNSToken()) and an FCM registration token (getToken()). Lane.Chat's push registration expects the FCM token on both platforms; a raw APNs token fails validation with a 422 and push silently never arrives.
Route notification taps back into the chat:
void openChatOnTap(Map<String, dynamic> data) {
if (!LaneChat.isLaneChatPush(data)) return; // not ours
LaneChat.handlePushTap(data); // stores the deep link
LaneChat.showChat(context); // delivers it once the widget is ready
}Flutter public API
| Member | Purpose |
|---|---|
LaneChat.initialize(appId, {config}) | Bootstrap. Call once at launch. |
LaneChat.identify(userId, {traits}) | Attach a logged-in user. |
setSessionData | Not available on Flutter yet — see Two levels of visitor data. |
LaneChat.showChat(context) | Push the full-screen chat page. |
LaneChatView / LaneChatChatPage | Embeddable / full-screen chat UI. |
LaneChat.setDeviceToken(token) | Forward an FCM token. |
LaneChat.setPushEnabled(enabled) | Opt this device out of / back into push. |
LaneChat.isLaneChatPush(data) / handlePushTap(data) | Push triage and tap handling. |
LaneChat.setLinkHandler(handler) | First step of link routing. |
LaneChat.events + typed streams | Widget event bus. |
LaneChat.track(eventName, {properties}) | Custom events. |
LaneChat.reset() | Clear identity on logout; restarts anonymous. |
LaneChatConfig fields: endpoint, widgetBaseUrl, linkPolicy, webView, debug — link routing works like the native SDKs (your handler first, then non-http(s) to the system, then http(s) per LaneChatHttpMode.inApp / .external).
Push: how the pieces fit
You never ship signing keys in the app. Upload your credentials once on the dashboard, and Lane.Chat's servers dispatch pushes to the tokens the SDK registers:
- Android / Flutter — in Firebase, generate a service-account private key (JSON) and upload it in the dashboard under Channels → App Push.
- iOS — enable push for your App ID, generate an APNs Auth Key (
.p8, preferred) or certificate (.p12), and upload it in the dashboard.
The server checks visitor presence before sending: while the app is in the foreground with a live connection, pushes for that visitor are suppressed — the user sees the in-app surface instead of a duplicate notification. setPushEnabled(false) unregisters the device token server-side and stops local rendering; setPushEnabled(true) re-registers it.
Two levels of visitor data
Agents see two layers of context next to a conversation, and the SDK has one method for each. Which one you want comes down to a single question: does this still matter the next time they get in touch?
identify(userId, traits) | setSessionData(data) | |
|---|---|---|
| Answers | Who is this person? | What is this conversation about? |
| Lifetime | Follows the user across every future conversation | This conversation only |
| Typical keys | plan, email, signup_date, mrr | order_id, cart_total, screen, error_code |
User data is the durable layer. Call identify() once at login and forget about it; the user id is persisted across app restarts and the traits are re-sent every time the chat opens, so a later call is always reflected.
Session data is the here-and-now. Set it as the user moves around your app, right before they reach out:
Android:
LaneChat.setSessionData(mapOf("order_id" to "X-42", "cart_total" to 129.5))
LaneChat.showChat(context)iOS / macOS:
LaneChat.setSessionData(["order_id": "X-42", "cart_total": 129.5])
LaneChat.showChat(from: self)Keys are merged, not replaced. A later call carrying screen leaves order_id and cart_total in place and adds screen, so each part of your app can contribute what it knows without resending everything. An existing key changes only when a later call names it, and passing a null value drops a key.
Both calls are cheap and never reload the chat, and both work the same whether the chat is open or closed. They do need the SDK running: call initialize() first — calls made before that are logged and dropped rather than queued.
Flutter
setSessionData is not available in the Flutter package yet. Put the values you need on identify() traits in the meantime; agents still see them, they just persist on the visitor instead of expiring with the conversation.
Shared identity across native and web
The SDK carries its device identity and your identify()'d user_id into the chat session, so the server reuses the same visitor record the SDK created — one device, one visitor identity across native and web. Traits show up for agents in the visitor panel. Call identify() before or after opening chat; the chat URL is rebuilt on every open, so the latest identity always applies.
Zero endpoint configuration
The SDK manages its own session with Lane.Chat's servers — you never configure a backend URL, chat host, or socket endpoint. The current chat host page and realtime endpoint are delivered by the server at session start and persisted by the SDK, so Lane.Chat can migrate infrastructure without you shipping an app update. Transient failures recover on their own: identity, events, and push registration wait for the session instead of being dropped, and an expired session is transparently re-established.
Session starts and custom events have per-app quotas; normal production traffic does not come near them.
Privacy
The SDKs collect only basic device characteristics (device id, OS, model, locale, timezone, screen, network type, app/SDK version). No advertising IDs, IMEI/OAID, carrier name, location, or contacts — nothing that requires an extra runtime permission. The same device resolves to the same visitor on every platform.
App store privacy declarations
Everything the SDK collects is for app functionality only — no tracking, no sharing with third parties, no advertising use. Copy these answers into the store forms as-is:
Google Play — Data safety form
- Device or other IDs → Collected, purpose App functionality, not shared, not processed ephemerally.
- If you call
identify(...): Personal info → User IDs → collected, App functionality, not shared. - Messages → Other in-app messages (the support chat itself) → collected, App functionality, not shared.
The Android SDK declares only INTERNET, ACCESS_NETWORK_STATE, and POST_NOTIFICATIONS; it does not use the advertising ID.
App Store Connect — App Privacy
- Identifiers → Device ID → collected, App Functionality, linked to the user, not used for tracking.
- If you call
identify(...): Identifiers → User ID → same answers. - User Content → Other User Content (chat messages) → collected, App Functionality, linked, no tracking.
The iOS package ships a privacy manifest (PrivacyInfo.xcprivacy) with these same declarations and NSPrivacyTracking = false, so Xcode's aggregated privacy report already accounts for the SDK. No App Tracking Transparency prompt is needed.