Skip to content

Flutter

Add the Giov chat to a Flutter app as a ready-made screen or on your own controller, with verified identity, attachments, unread counts and push notifications.

On this page

The Flutter SDK (giovchat, version 0.1) shows the same conversation your website visitors have as a native screen. It matches the React Native SDK and uses the same Giov visitor protocol. File picking, secure storage and push tokens use your app's own packages through small adapters, so you add only the plugins you need.

Colours, greeting, pre-chat fields, GiovBot's name and avatar, attachment rules and branding come from Settings → Chat widget in the dashboard. In code, tenantKey is your workspace ID, shown in Settings → Chat widget → Installation.

Install

Requires Flutter 3.22 or newer (Dart 3.4).

Terminal
flutter pub add giovchat

The package depends only on http, http_parser, web_socket_channel and url_launcher. It uses Material icons, so keep uses-material-design: true in your pubspec.yaml (the default for new apps).

Platform setup

The chat itself needs network access and nothing else.

  • Android: release builds need <uses-permission android:name="android.permission.INTERNET"/> in android/app/src/main/AndroidManifest.xml. Flutter's template declares it only for debug and profile builds.
  • macOS: add <key>com.apple.security.network.client</key><true/> to both macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements.
  • iOS, web, Windows, Linux: nothing to add.

Links and attachments open with url_launcher, so you don't need <queries> or LSApplicationQueriesSchemes entries. To open them yourself, pass onOpenUrl.

Web

  • Inline images (attachments and teammate avatars) load with Image.network. With the CanvasKit and skwasm renderers the browser fetches the bytes itself, so the image host must send CORS headers (Access-Control-Allow-Origin). A blocked image shows a placeholder icon (an avatar falls back to the initial); tapping it still opens the file.
  • flutter_secure_storage on web keeps data in browser storage, which is not a hardware-backed keystore. Leave the default in-memory session store if the conversation doesn't need to survive a reload.
  • Push registration is for APNs and FCM device tokens from mobile apps.

Show the chat

The chat is a widget. Put it in any route, sheet or tab.

support.dart
import 'package:flutter/material.dart';
import 'package:giovchat/giovchat.dart';

void openSupport(BuildContext context) {
  Navigator.of(context).push(MaterialPageRoute<void>(
    builder: (context) => Scaffold(
      body: GiovChat(
        tenantKey: 'YOUR_WORKSPACE_ID',
        onClose: () => Navigator.of(context).pop(), // Shows a close button.
      ),
    ),
  ));
}

GiovChat creates, starts and disposes its own controller. It draws every state (loading, pre-chat form, greeting, connecting, active conversation, error with retry), passes app lifecycle changes to the controller, and keeps content clear of notches and the keyboard. Inside a bottom sheet that already does that, pass safeArea: false.

A conversation starts when the visitor sends their first message or submits the pre-chat form. A stored conversation is resumed first; a closed one is forgotten, and the next message starts a new one.

Controller or widget

GiovChat is a thin screen on top of GiovChatController, a ChangeNotifier that runs the whole visitor session: phase, messages, header identity, typing, unread count, errors and uploads. Use the widget for the ready-made screen, the controller alone for a custom UI, or both:

chat_controller.dart
final chat = GiovChatController(tenantKey: 'YOUR_WORKSPACE_ID', keepConnected: true, visible: false);
await chat.start();

// Later, anywhere: the ready-made screen on the controller you own.
GiovChat(controller: chat, autoStart: false, onClose: () => Navigator.pop(context));

Pass either controller or tenantKey to GiovChat, not both. With a controller you own, the widget never disposes it, so call chat.dispose() yourself. When the screen is removed, the widget marks the chat as not visible, so a keepConnected controller keeps counting unread replies.

Signed-in users

Pass the user's details through contact. Any field you provide skips the matching pre-chat field.

chat.dart
GiovChat(
  tenantKey: 'YOUR_WORKSPACE_ID',
  contact: GiovContact(
    userId: user.id,
    userHash: user.giovUserHash, // computed on your server
    email: user.email,
    firstName: user.firstName,
  ),
  sessionKey: user.id,
)
  • userHash is the HMAC-SHA256 (lowercase hex) of userId, keyed with the identity secret from Settings → Chat widget → Visitor details. Compute it on your server and send it to the app; never ship the secret. See Identify visitors for server examples.
  • Without userHash, userId is recorded as an unverified claim and doesn't link the chat to an existing contact.
  • A stored conversation is keyed by the workspace, sessionKey and userId only. Editing a name, email or phone keeps it. Changing tenantKey, contact.userId or sessionKey on the widget starts a fresh controller.
  • Sign-out and account switching: first await chat.unregisterPushToken(token) if you registered one, then change sessionKey or call reset(). The previous user's session, queued messages, unread count and remembered push token are dropped, and the next user starts fresh.

With the widget's own controller, reach reset() and the controller through a GlobalKey<GiovChatState>:

sign_out.dart
final chatKey = GlobalKey<GiovChatState>();

// GiovChat(key: chatKey, tenantKey: 'YOUR_WORKSPACE_ID', ...)

Future<void> signOut(String pushToken) async {
  await chatKey.currentState?.controller.unregisterPushToken(pushToken);
  await chatKey.currentState?.reset();
}

Keep the conversation across launches

By default nothing is stored, and the conversation is forgotten when the app process ends. Pass a GiovSessionStore as sessionStore to resume the same conversation next time. Implement read, write and delete with secure storage you already have:

secure_session_store.dart
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:giovchat/giovchat.dart';

class SecureSessionStore implements GiovSessionStore {
  const SecureSessionStore([this._storage = const FlutterSecureStorage()]);
  final FlutterSecureStorage _storage;

  // A stable key per scope (hashCode is not stable across runs and may collide).
  String _key(String scope) => 'giovchat.${base64Url.encode(utf8.encode(scope))}';

  @override
  Future<GiovSession?> read(String scope) async {
    final value = await _storage.read(key: _key(scope));
    return value == null ? null : GiovSession.fromJson(jsonDecode(value) as Map<String, Object?>);
  }

  @override
  Future<void> write(String scope, GiovSession session) =>
      _storage.write(key: _key(scope), value: jsonEncode(session.toJson()));

  @override
  Future<void> delete(String scope) => _storage.delete(key: _key(scope));
}

// GiovChat(tenantKey: 'YOUR_WORKSPACE_ID', sessionStore: const SecureSessionStore());
Warning

The session token is a private credential. Keep it in the Keychain or Keystore, never in plain SharedPreferences, never log it, and never share it between users.

If your store throws, the SDK reports a GiovStorageException through onError and carries on without the stored session.

Attachments

Attachments appear once the conversation has started, when you pass an attachmentPicker and the published GiovBot agent allows them. Visitors can pick only the allowed types and sizes, up to 5 files per message, and the server checks again.

The SDK ships no picker. attachmentPicker is a function that receives a GiovPickerRequest (allowedTypes, maxFiles, maxSizeMb) and returns a list of GiovPickedFile, or an empty list when the visitor cancels. Create a file with GiovPickedFile.bytes (the bytes are in memory) or GiovPickedFile.stream (read lazily, size required).

pickers.dart
import 'package:file_picker/file_picker.dart';
import 'package:giovchat/giovchat.dart';
import 'package:mime/mime.dart';

Future<List<GiovPickedFile>> pickFiles(GiovPickerRequest request) async {
  final result = await FilePicker.platform.pickFiles(allowMultiple: request.maxFiles > 1, withData: true);
  if (result == null) return const [];
  return [
    for (final file in result.files.take(request.maxFiles))
      if (file.bytes != null)
        GiovPickedFile.bytes(
          name: file.name,
          mimeType: lookupMimeType(file.name) ?? 'application/octet-stream',
          bytes: file.bytes!,
        ),
  ];
}

Then pass it: GiovChat(tenantKey: 'YOUR_WORKSPACE_ID', attachmentPicker: pickFiles). Files with an unknown type are refused with a message. Uploads show progress in the composer. Images appear inline in a box of their final shape, and other files as rows.

To let everyone, including your team, see an image's shape before it loads, pass width and height when you know them. You can also pass a small thumbnail: a data:image/jpeg;base64,... string of an image about 32 px wide, at most 6,000 characters. A larger one isn't used and the image loads in place.

Picker plugin setup

Each plugin's own README is the reference for its setup. What they need:

Platformimage_picker (photos)file_picker (documents)
iOS (ios/Runner/Info.plist)NSPhotoLibraryUsageDescription; also NSCameraUsageDescription if you take photos and NSMicrophoneUsageDescription if you record videoNSPhotoLibraryUsageDescription if you pick from the photo library (FileType.image or media); none for documents
AndroidNothing (it uses the system photo picker)Nothing (it uses the system document picker)
macOS (both .entitlements files)com.apple.security.files.user-selected.read-onlycom.apple.security.files.user-selected.read-only
WebWorks; files come back as bytes or a streamWorks; use withData: true, file paths are not available

For example, in Info.plist:

ios/Runner/Info.plist
<key>NSPhotoLibraryUsageDescription</key>
<string>Choose photos to send in the support chat.</string>
<key>NSCameraUsageDescription</key>
<string>Take a photo to send in the support chat.</string>

Unread replies and badges

By default the chat connects only while it's visible. To count replies that arrive while it's closed, keep a controller alive with keepConnected: true and visible: false. It only resumes an existing conversation and never starts one in the background.

Read the count anywhere with GiovUnread.watch, without passing the controller around. Omit the key to sum all workspaces.

chat_badge.dart
ValueListenableBuilder<int>(
  valueListenable: GiovUnread.watch('YOUR_WORKSPACE_ID'),
  builder: (context, unread, _) => Badge(
    isLabelVisible: unread > 0,
    label: Text('$unread'),
    child: const Icon(Icons.chat_bubble_outline),
  ),
)

Opening the chat clears the count. To react to changes in code, use onUnreadCountChange or GiovUnread.read('YOUR_WORKSPACE_ID').

Push notifications

  1. Add your Apple push key and/or Firebase service account in Settings → Chat widget → Push notifications. Notifications are sent from your app, with your keys. See Push credentials.
  2. Register each device token with the SDK:
push.dart
final token = await FirebaseMessaging.instance.getToken();
if (token != null) {
  await chat.registerPushToken(
    token,
    GiovPushPlatform.fcm, // or GiovPushPlatform.apns for a raw APNs token
    options: GiovPushOptions(
      // APNs only: development builds get sandbox tokens.
      environment: kDebugMode ? GiovPushEnvironment.sandbox : GiovPushEnvironment.production,
    ),
  );
}
FirebaseMessaging.instance.onTokenRefresh.listen((token) => chat.registerPushToken(token, GiovPushPlatform.fcm));

chat is your GiovChatController, or the controller of a widget (chatKey.currentState?.controller). GiovChatState also has registerPushToken directly.

  • Pass the raw device token: the APNs token on iOS (or the FCM token if your iOS app uses Firebase Messaging), and the FCM token on Android. Register again whenever the token changes.
  • Use GiovPushEnvironment.sandbox from iOS debug builds (Xcode installs get sandbox APNs tokens). TestFlight and App Store builds use production.
  • It registers straight away when connected, otherwise as soon as the conversation connects, and never throws. It returns a GiovPushResult: check registered, or failure (GiovPushFailure.noConversation, unsupported or failed).
  • Before any conversation exists, the call waits for the first one (the visitor's first message). Don't block app start on it. Disposing the controller resolves it as GiovPushFailure.noConversation.
  • The token is remembered. When the team closes a conversation and the visitor starts a new one, the device is registered for it automatically.
  • A tapped notification carries {type: 'giov.message', tenantId, conversationId, messageId}, under giov on APNs and in data on FCM. Open the chat screen and it catches up from history.

On sign-out, call await chat.unregisterPushToken(token) before reset() or changing sessionKey. It works while the chat is closed or disconnected, because it uses the stored session. reset() forgets that session and the remembered token.

Language

The interface follows the device language: English, French, Spanish, Portuguese or German, with English for others. Text you write in the dashboard (greeting, placeholder, bot name) is shown as written.

chat.dart
GiovChat(
  tenantKey: 'YOUR_WORKSPACE_ID',
  locale: 'pt-BR',
  strings: (base) => base.copyWith(sendMessage: 'Enviar', startChat: 'Começar'),
)

Replace any string through copyWith; empty values are ignored. giovStrings exports the built-in tables. With a controller you own, the language is the controller's (GiovChatController(locale: ...)) unless you set locale or strings on GiovChat.

Appearance

  • Theme: the dashboard setting (light, dark, auto) applies, and auto follows the device. Override it with theme: GiovThemeMode.light, dark or auto.
  • Colour: the dashboard's main colour applies. Override it with primaryColor. Text on it switches between white and near-black to stay readable.
  • Fonts and surface colours: styleOverrides.
chat_styles.dart
GiovChat(
  tenantKey: 'YOUR_WORKSPACE_ID',
  styleOverrides: const GiovChatStyleOverrides(
    fontFamily: 'YourAppFont', // Register this font in your app first.
    container: Color(0xFFF8FAFC),
    header: Color(0xFF0F172A),
    headerTitle: GiovTextOverride(color: Colors.white, fontWeight: FontWeight.w700),
    headerSubtitle: GiovTextOverride(color: Color(0xFFCBD5E1)),
    agentBubble: Color(0xFFE2E8F0),
    agentText: GiovTextOverride(color: Color(0xFF0F172A)),
    visitorBubble: Color(0xFF2563EB),
    visitorText: GiovTextOverride(color: Colors.white),
    composer: Color(0xFFF8FAFC),
    input: GiovTextOverride(color: Color(0xFF0F172A)),
    inputBackground: Colors.white,
    button: Color(0xFF2563EB),
    buttonText: GiovTextOverride(color: Colors.white),
  ),
)

Slots that take a Color set a background. Slots that take a GiovTextOverride accept fontFamily, fontWeight, fontStyle and color.

SlotsTypeApplies to
container, header, composer, formColorChat surface, header, composer row, pre-chat form
agentBubble, visitorBubbleColorAgent, greeting and typing bubbles; visitor bubbles
buttonColorSend, start-chat and retry buttons
headerTitle, headerSubtitleGiovTextOverrideHeader text; the title colour also applies to the close icon
agentText, visitorTextGiovTextOverrideMessage content, sender labels and timestamps; agent text also covers greeting and typing
formTitle, label, buttonText, hint, errorTextGiovTextOverrideForm heading and labels, button text, loading hint, attachment chips, errors; the hint also styles input placeholders
inputGiovTextOverrideText in the composer and all pre-chat inputs
inputBackgroundColorBackground of the composer and pre-chat inputs

The top-level fontFamily is the default for customizable text, and a slot's fontFamily takes precedence. Font sizes, spacing, borders, dimensions and the branding footer stay under the SDK's control.

Branding: Starter and Pro always show Powered by GiovChat. Business and Advanced can turn it off in Settings → Chat widget → Appearance. The footer shows unless the workspace explicitly turns it off, and style overrides don't affect its look. If you build your own UI on the controller, honour controller.config?.showBranding yourself.

Lifecycle and errors

  • Background: when the app goes to the background, the connection stays open for 5 seconds and then closes, so push notifications can reach the device. Change that with backgroundGrace on GiovChatController. When the app returns, the chat reconnects and catches up on anything it missed.
  • Dropped connections: the SDK retries with a growing delay and shows a "Connection lost. Reconnecting…" notice. The composer stays editable.
  • Outbox: messages typed offline or while reconnecting wait in an outbox and go out in order once the connection is back, each under one ID so it's never stored twice. A message that still fails shows Not sent · Tap to retry, with a Discard button.
  • Message length: messages are limited to 2,000 UTF-16 code units. A longer paste is cut at the limit.
  • Errors: onError receives every failure as a GiovChatException. Subtypes include GiovNetworkException, GiovSessionExpiredException, GiovConversationClosedException, GiovRateLimitedException, GiovWorkspaceBusyException, GiovIdentityRejectedException, GiovAttachmentRejectedException, GiovSendFailedException, GiovStorageException and GiovServerException. Their message is for logs, in English. The screen shows its own translated text.

Reference

GiovChat

ParameterTypeDefaultDescription
tenantKeyString?noneYour workspace ID. Pass this or controller
controllerGiovChatController?noneA controller you own. The widget never disposes it
baseUrlStringhttps://api.giovchat.appAPI base URL override
contactGiovContact?noneSigned-in user's details; also pre-fills the pre-chat form
sessionKeyString?noneChange it to isolate or reset the session (sign-out, account switch)
sessionStoreGiovSessionStore?in memoryYour secure storage for the session
keepConnectedboolfalseStay connected while hidden to count unread replies
visiblebooltrueWhether the chat is on screen. A hidden chat disconnects unless keepConnected; showing it clears unread replies
autoStartbooltrueStart the controller on mount. Starting an already started controller does nothing
onCloseVoidCallback?noneShows a close button in the header
themeGiovThemeMode?From dashboardlight, dark or auto; overrides the dashboard
primaryColorColor?From dashboardBrand colour override
styleOverridesGiovChatStyleOverridesnoneAllowed font and colour overrides
localeString?Device languageInterface language
stringsGiovChatStrings Function(GiovChatStrings)?noneReplace any interface string
attachmentPickerGiovAttachmentPicker?noneEnables attachments, when the agent allows them
onOpenUrlGiovUrlOpener?url_launcherHow links and attachments open. Only http(s) and mailto links reach it
safeAreabooltrueKeep clear of notches and the home indicator
onErrorvoid Function(GiovChatException)?noneEvery failure
onConversationStartvoid Function(String)?noneCalled with the conversation ID
onConnectionStatusChangevoid Function(GiovConnectionStatus)?noneconnecting, connected or disconnected
onUnreadCountChangevoid Function(int)?noneUnread replies while hidden

baseUrl, contact, sessionKey, sessionStore, keepConnected and the callbacks configure the controller the widget creates; they are ignored when you pass controller, except contact, which still pre-fills the form.

GiovChatState (through a GlobalKey<GiovChatState>) has controller, reset(), which clears this user's session and starts fresh, and registerPushToken(token, platform, {options}).

GiovChatController

MemberDescription
GiovChatController({required tenantKey, baseUrl, contact, sessionKey, sessionStore, keepConnected, visible, locale, overrideStrings, backgroundGrace, onError, onConversationStart, onConnectionStatusChange, onUnreadCountChange})Creates a controller. keepConnected defaults to false, visible to true, backgroundGrace to 5 seconds
start()Loads the configuration and resumes a stored conversation. Does nothing if already started
retry()Starts over after an error, or forces a fresh connection
reset()Forgets the conversation, stored session and outbox
setVisible(bool)Shows or hides the chat; showing clears unread replies
handleLifecycle(state)Forward AppLifecycleState changes here when you build your own UI
send(text)Sends a message
registerPushToken(token, platform, {options})Registers the device for push. Never throws
unregisterPushToken(token)Stops push for this device. Works while the chat is closed
dispose()Releases the controller. Call it yourself when you own it

State to read from it: phase (GiovChatPhase), messages, header, agentTyping, unreadCount, error, connectionStatus, conversationId, config and uploads.

Loading…