karam_messenger 0.2.0
karam_messenger: ^0.2.0 copied to clipboard
Karam messenger for Flutter: presents Karam's hosted customer-support messenger in a secured WebView, with identity, deep links and unread counts.
karam_messenger (Flutter) #
Karam's customer-support messenger for Flutter apps. The SDK shows Karam's hosted messenger in a locked-down WebView and handles presentation, dismissal, loading and error states, the keyboard, safe areas, Android back, attachments, RTL and dark mode. You call a handful of methods.
- Android (minSdk 24) and iOS (13+; Flutter 3.47 projects default to 15). Web and desktop are not supported.
- Dart
^3.13, Flutter>=3.47. - No custom platform channels. It is built on
webview_flutter,shared_preferences,http,url_launcher,file_selectorandimage_picker.
Building a Zid merchant app with AppsBunches? Use
package:karam_messenger/appsbunches.dart and follow the
AppsBunches integration guide
instead of the steps below.
Install #
flutter pub add karam_messenger
1. Place the host once #
The host widget draws the messenger. Put it around your app's navigator, in the
app's builder:
MaterialApp(
builder: (context, child) => KaramMessengerHost(child: child!),
home: const HomeScreen(),
)
This works the same with MaterialApp.router (go_router and similar), CupertinoApp
and WidgetsApp. When you call present(), the host pushes a full-screen route onto
your root navigator. That route covers nested navigators' tab bars and any open
dialogs, gets Android back and predictive back, and hides your screens from screen
readers while it is open. Back closes the messenger. It never pops your screen
underneath.
Without a host, pass a BuildContext that has a Navigator above it:
KaramMessenger.instance.present(context: context). The messenger is pushed
onto that context's root navigator and behaves the same way.
2. Initialise #
void main() {
KaramMessenger.instance.init(const KaramMessengerConfig(
appId: 'app_…', // Karam portal → Settings → Developers
locale: MessengerLocale.ar, // optional: ar | en (default: the page decides)
colorScheme: MessengerColorScheme.system, // optional: light | dark | system
));
runApp(const MyApp());
}
messengerUrl and apiUrl override the Karam endpoints for staging or local
development only. They must be https. Plain http is accepted only for
localhost and 127.0.0.1. Calling init again closes the messenger and switches to
the new configuration. The current identity is kept.
3. Identify signed-in users #
Karam identifies users with an identity token: a JWS (HS256) that your backend signs with the app's identity secret. Never put the secret in the app.
| claim | |
|---|---|
sub |
required: your stable user id (1–255 chars) |
aud |
required: the appId |
exp |
required: at most 1 hour ahead |
iat |
optional |
name, email, phone |
optional profile enrichment. Karam never matches users by these. |
KaramMessenger.instance.identify(tokenProvider: () async {
// Call YOUR backend, authenticated with YOUR session.
final response = await myApi.post('/karam/identity-token');
return response.data['token'] as String;
});
The SDK calls tokenProvider only when it needs a token: when the messenger opens,
when Karam's session expires, and for getUnreadCount(). It never stores the token
and never puts it in a URL. A provider that throws, returns an empty or oversized
token (over 8192 chars), or takes longer than 15 seconds shows the native error view
with Try again.
Calling identify for a different user while the messenger is open reloads the
messenger. Nothing from the previous user carries over, and a late answer meant for
the previous user is discarded.
Visitors who never call identify are anonymous. Their conversation is tied to this
installation.
4. Present, dismiss, deep-link #
KaramMessenger.instance.present(); // open
KaramMessenger.instance.present(conversationId: '3f0c7a2e-…'); // open a conversation
KaramMessenger.instance.dismiss(); // close
conversationId must be a UUID, otherwise present throws
KaramMessengerException(invalid_options). If the messenger is already open, the
conversation opens inside it. If it is still loading, the conversation opens as soon
as it is ready. A conversation id from a push notification only opens that user's
own conversations.
5. Unread count #
try {
final unread = await KaramMessenger.instance.getUnreadCount();
} on KaramMessengerException catch (e) {
switch (e.code) {
case KaramMessengerErrorCode.cancelled: break; // identify/logout ran meanwhile: ignore
case KaramMessengerErrorCode.notIdentified: break; // anonymous visitors have no count
default: /* identity_unavailable, identity_rejected, network */
}
}
The messenger does not need to be open. The count is never reported for a user who has since logged out or been replaced.
6. Logout #
await KaramMessenger.instance.logout();
Logout closes the messenger, forgets the identity and starts a new anonymous visitor.
It rotates the installation id: karam.messenger.installationId in
SharedPreferences is the only key the SDK writes. If the messenger page is open, it
also clears that page's own localStorage/sessionStorage. It does not touch any
other app data, cookies or WebViews.
If the messenger was closed at logout, the previous visitor's anonymous session can
remain in the messenger origin's localStorage until the next time the messenger
opens. The page then purges every key that does not belong to the new installation
id before it renders anything. Identified sessions are never persisted by the page.
Events and state #
KaramMessenger.instance.events.listen((event) {
switch (event) {
case MessengerPresentedEvent(): …
case MessengerDismissedEvent(): … // a good moment to refresh the unread badge
case MessengerErrorEvent(:final code, :final retryable): …
}
});
KaramMessenger.instance.presentation; // ValueListenable<PresentationState>
MessengerErrorEvent is one of two kinds:
MessengerPageErrorEvent: the page reportedconfig_unavailable,identity_rejected,conversation_not_found,networkorinternaland draws its own UI.MessengerPresentationFailedEvent: the SDK shows its native error view, forload_failed,timeoutoridentity_unavailable.
Attachments: platform setup #
iOS: add these keys to Info.plist. WKWebView presents the system picker and
camera itself.
<key>NSCameraUsageDescription</key>
<string>Take a photo to send to support.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Choose a photo to send to support.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Record a video to send to support.</string>
Android: the SDK answers <input type="file"> with the system document picker
(file_selector), or with the camera (image_picker) when the input asks for a
capture. It needs no storage permission. In AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET"/>
<queries>
<intent><action android:name="android.intent.action.VIEW"/><data android:scheme="https"/></intent>
<intent><action android:name="android.intent.action.SENDTO"/><data android:scheme="mailto"/></intent>
<intent><action android:name="android.intent.action.DIAL"/><data android:scheme="tel"/></intent>
<intent><action android:name="android.media.action.IMAGE_CAPTURE"/></intent>
</queries>
INTERNET is only merged into debug builds by default, so release builds need it
declared explicitly. If your app declares the CAMERA permission, Android requires
you to have been granted it at runtime before any app, including this SDK, can open
the camera.
Layout behaviour #
- Safe areas. On iOS the WebView runs edge to edge and the page pads itself
with
env(safe-area-inset-*). On Android the SDK lays the WebView out inside the safe area and tells it to ignore system-bar, cutout and IME insets, so nothing is padded twice. The native loading and error views useSafeAreaon both platforms. - Keyboard. The page area shrinks above the keyboard, which is equivalent to
resizeToAvoidBottomInset. Android needsandroid:windowSoftInputMode="adjustResize"on the activity, which is Flutter's default. - RTL and dark mode. The native views follow
locale, or your app's locale whenlocaleis not set, andcolorScheme, wheresystemfollows the platform brightness.
Security model #
- The WebView loads only the messenger origin. Other
https,mailto:andtel:links open in the system browser or handler. Every other scheme, includingjavascript:,data:,file:,intent:andblob:, is refused. Foreign iframes are refused on iOS. - The page URL carries only
appId,platform,protocolandsdkVersion. Tokens travel only over the bridge, only after the page announcesready, and only to the messenger origin. webview_flutterJavaScript channels don't report the sending frame. The SDK attributes each bridge message to the origin of the WebView's current URL, which the navigation policy restricts to the messenger origin. Every message is parsed by the protocol v1 parser. Anything malformed or unknown is dropped.- Web permission requests (camera, microphone, geolocation) are denied. File access is off on Android. The WebView is inspectable only in debug builds.
Troubleshooting #
| Symptom | Cause |
|---|---|
Assertion: present() needs a KaramMessengerHost |
No host in the tree and no context passed. See step 1. |
KaramMessengerHost found no Navigator below it |
The host was placed below your navigator or in a subtree without one. Use the app builder. |
invalid_options from init |
Blank appId, or a non-https messengerUrl/apiUrl. |
| Error view "Couldn't sign you in" | tokenProvider threw, timed out (15 s) or returned an empty or oversized token. Check your backend. |
| Error view "This is taking too long" | The page didn't answer within 15 s: very slow network, or a messengerUrl that isn't the Karam messenger. |
| Page shows "identity rejected" | Karam refused the token: wrong secret, aud not the appId, or exp more than 1 hour ahead. |
| Links do nothing on Android 11+ | Missing <queries> entries (see above). |
| Blank page on Android release builds | Missing INTERNET permission. |
Debug builds print operational logs prefixed [KaramMessenger]. They never contain
tokens or message contents.
Development #
cd sdks/flutter
flutter test # conformance fixtures, controller scenarios, host widget tests
flutter analyze
test/protocol_conformance_test.dart reads the shared conformance suite from the
monorepo at ../../packages/messenger-bridge/fixtures/protocol-v1.json, relative to
this package. flutter test runs from the package root. The Dart parser
(lib/src/protocol.dart) and controller (lib/src/controller.dart) are ports of
packages/messenger-bridge/src/{protocol,controller}.ts. The TypeScript is
normative. Keep the ports in step with it, method for method.
example/ is a small app with a placeholder appId and token endpoint. Replace both
constants in example/lib/main.dart.