FrontFace Chat for Flutter
Native Flutter SDK for FrontFace AI chat with optional human handoff. No WebView required.
Features
- AI chat powered by the FrontFace Mobile SDK API
- Lead capture form (configurable from the FrontFace dashboard)
- Human agent handoff with live message polling, agent typing (Realtime), and customer typing/presence for the dashboard
- Support tickets (structured actions + ticket cards), CSAT ratings, offline contact form
- Channel launcher buttons (WhatsApp, email, phone, etc.) from dashboard config
- Customer identity verification via backend-signed JWT (
identify/resetUser) - Optional attachments: location (Google Maps), images, voice notes (config-gated; uploaded via FrontFace signed URLs)
- Session restore across app restarts (session id + session token persisted automatically)
- Customizable theme and localized strings
- One-line
FrontFaceChat.open()integration
Requirements
- Flutter 3.22+
- Dart 3.8+
- A FrontFace account with Mobile SDK enabled
Installation
Add the package to your pubspec.yaml:
dependencies:
frontface_chat: ^1.5.2
For local development (this repo lives next to your app):
dependencies:
frontface_chat:
path: ../frontface_chat
Then run:
flutter pub get
Get your credentials
- Sign in to the FrontFace dashboard.
- Open your project → Mobile SDK.
- Copy:
- Project ID — UUID (e.g.
92b2d515-0000-45a7-8b0a-df20a33ceb2a) - Publishable key — starts with
pk_
- Project ID — UUID (e.g.
Do not confuse
projectIdwithpublishableKey. They are different values.
Quick start
The fastest way to add chat is a floating action button or a button tap:
import 'package:frontface_chat/frontface_chat.dart';
const config = FrontFaceChatConfig(
projectId: 'YOUR_PROJECT_UUID',
publishableKey: 'pk_YOUR_KEY',
);
// Open chat from anywhere
await FrontFaceChat.open(context, config: config);
// Or use the built-in FAB
floatingActionButton: FrontFaceChat.fab(context, config: config),
Integration patterns
1. One-shot (recommended for most apps)
FrontFaceChat.open() creates a scoped provider, pushes the chat screen, and cleans up when the user closes it. No global setup needed.
2. Global Provider
If you already use provider and want chat state shared or custom navigation:
// In main.dart / app setup
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => FrontFaceChat.createProvider(config: config),
),
],
child: MyApp(),
)
// Navigate manually
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const FrontFaceChatScreen(),
),
);
3. Custom FAB / button
IconButton(
icon: const Icon(Icons.support_agent),
onPressed: () => FrontFaceChat.open(context, config: config),
)
4. Multiple projects (white-label / multi-tenant apps)
Each FrontFaceChatConfig is scoped to one FrontFace project. Pass a different projectId + publishableKey per tenant — session, session token, and lead-form state are stored per projectId, so multiple configs can coexist in one app:
final supportConfig = FrontFaceChatConfig(
projectId: tenantAProjectId,
publishableKey: tenantAPublishableKey,
);
final salesConfig = FrontFaceChatConfig(
projectId: tenantBProjectId,
publishableKey: tenantBPublishableKey,
);
// Open the right chat for the active tenant
await FrontFaceChat.open(context, config: supportConfig);
Visitor id is shared device-wide (one mob_* id) by default. For logged-in users, pass a stable account-keyed visitorId from your backend so history follows the user across reinstalls/devices (see CHAT_HISTORY_GUIDE.md). Then call identify so agents see a verified contact.
final provider = FrontFaceChat.createProvider(
config: FrontFaceChatConfig(
projectId: projectId,
publishableKey: pk,
visitorId: accountVisitorIdFromBackend, // from Freshhouse backend
),
);
await provider.initialize();
// After your login API returns `frontfaceToken`:
try {
await FrontFaceChat.identify(
provider: provider,
identityToken: frontfaceToken,
);
} on FrontFaceIdentifyException catch (e) {
// Never block chat — log and retry with a fresh token on next login
debugPrint('Identify failed: ${e.code}');
}
On logout:
await FrontFaceChat.resetUser(provider);
Customization
Attachments (optional)
All attachment types are off by default. Enable only what you need.
Images and voice notes upload through FrontFace (POST /api/media/uploads →
signed PUT → parts on POST /api/chat/message) — no host uploader.
Location is sent as a structured location object (not a Maps URL in text).
final config = FrontFaceChatConfig(
projectId: projectId,
publishableKey: pk,
attachments: FrontFaceAttachmentsConfig(
enableLocation: true,
enableImages: true,
enableAudio: true,
// Optional — with a key: map picker + place search + static previews.
// Without a key: user can still share current GPS location.
googleMapsApiKey: 'YOUR_GOOGLE_MAPS_API_KEY',
),
);
See INTEGRATION_GUIDE.md §5.3–5.5 for the location / image / voice API contracts.
Native setup (host app):
- Android
AndroidManifest.xml: location / camera / mic permissions. If you use a Maps key, also addcom.google.android.geo.API_KEYand enable Maps SDK, Places API, and Geocoding API on that key. Gallery uses the system Photo Picker (noREAD_MEDIA_IMAGESrequired for picking). - iOS
Info.plist:NSLocationWhenInUseUsageDescription,NSCameraUsageDescription,NSPhotoLibraryUsageDescription,NSMicrophoneUsageDescription. With a Maps key, callGMSServices.provideAPIKeyinAppDelegate. - iOS Podfile — enable
permission_handlermacros or every request looks permanently denied (open Settings loop):
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_CAMERA=1',
'PERMISSION_PHOTOS=1',
'PERMISSION_MICROPHONE=1',
'PERMISSION_LOCATION=1',
]
end
end
end
Then run cd ios && pod install.
Permissions use the native system dialog. An in-app popup appears only if access is permanently denied (to open Settings) or location services are off.
Theme
Match chat colors to your brand. Bubble colors are optional — override only what you need:
await FrontFaceChat.open(
context,
config: config,
theme: const FrontFaceChatTheme(
primaryColor: Color(0xFF0F172A),
// Optional — visitor (user) bubbles
userBubbleColor: Color(0xFF2563EB),
userBubbleTextColor: Colors.white,
// Optional — agent / assistant bubbles
assistantBubbleColor: Colors.white,
assistantBubbleTextColor: Color(0xFF0F172A),
assistantBubbleBorderColor: Color(0xFFE2E8F0),
onlineIndicatorColor: Color(0xFF17B26A),
),
);
Available theme properties: primaryColor, onPrimaryColor, backgroundColor,
inputBackgroundColor, userBubbleColor, userBubbleTextColor,
assistantBubbleColor, assistantBubbleTextColor, assistantBubbleBorderColor,
subtitleColor, errorColor, onlineIndicatorColor, agentNameColor, linkColor,
fontFamily, arabicFontFamily.
For Arabic / RTL (FrontFaceChatStrings.arabic), the SDK uses the bundled
Noto Sans Arabic family (FrontFaceArabic) by default. Override if needed:
theme: FrontFaceChatTheme(
arabicFontFamily: 'FrontFaceArabic', // default
// fontFamily: 'YourLatinFont', // optional LTR font
),
Bottom safe area / host bottom navigation
The composer and action buttons always clear the system home indicator. If your
app embeds chat under a bottom nav bar, pass its height as extraBottomInset:
await FrontFaceChat.open(
context,
config: config,
extraBottomInset: 80, // your BottomNavigationBar height
);
// Or when using the screen directly:
FrontFaceChatScreen(theme: theme, extraBottomInset: 80);
Strings (i18n) and RTL
Every user-visible label (attachments, permissions, tickets, CSAT, offline form, image viewer, etc.) lives on FrontFaceChatStrings. Prefer the built-in packs so you do not re-translate every field:
// English (default)
await FrontFaceChat.open(context, config: config);
// Full Arabic pack (RTL + all labels)
await FrontFaceChat.open(
context,
config: config,
strings: FrontFaceChatStrings.arabic,
);
// Or resolve from a language code (`en`, `ar`, …)
await FrontFaceChat.open(
context,
config: config,
strings: FrontFaceChatStrings.forLanguage(
Localizations.localeOf(context).languageCode,
title: 'Support', // optional override
),
);
Override individual keys when needed:
final strings = FrontFaceChatStrings.arabic.copyWith(
attach: 'إضافة مرفق',
title: 'المساعدة',
);
Partial updates at runtime:
provider.updateStrings(
FrontFaceChatStrings.forLanguage('ar').copyWith(title: 'الدعم'),
);
What's client-side vs. dashboard-driven: greeting, placeholder, and the chat
title normally come from the FrontFace dashboard (config.title, etc.) — whatever
language the project owner configured there. Lead-form field 2/3 labels (e.g. "Phone
Number") also come from the dashboard — override them with strings.field2Label /
strings.field3Label for English or Arabic. If the dashboard value is empty, the SDK
falls back to strings.typeMessage / strings.talkToHuman / strings.loadingChat. The
one exception is strings.title: since the dashboard title is almost never empty (it
usually says something like "Support"), set strings.title explicitly to force a
client-side translation that always wins over the dashboard value.
Message bubbles and the input field also auto-detect their own text direction from
content, so English typed into an Arabic-configured chat (or vice versa) renders
correctly regardless of the chat's overall textDirection.
Changing language at runtime
strings is normally fixed for the lifetime of a FrontFaceChatProvider, but if your
app supports switching language while the chat is already open (e.g. a language toggle
in settings), call updateStrings() on the provider you're using with createProvider()
or context.read<FrontFaceChatProvider>():
provider.updateStrings(FrontFaceChatStrings.forLanguage('ar'));
This swaps every string immediately — title, placeholder, input direction, and any already-shown status banner (e.g. "Waiting for an agent...") all re-render in the new language without recreating the provider or screen.
Debug logging
Enable HTTP request logs during debug builds only:
const config = FrontFaceChatConfig(
projectId: '...',
publishableKey: 'pk_...',
debugLogging: true, // ignored in release/profile — never logs in production
);
Secrets (X-FrontFace-Key, session tokens) are redacted from curl logs. Leave
debugLogging: false (the default) in production configs.
| Option | Required | Notes |
|---|---|---|
requireLeadCaptureBeforeChat |
No | Defaults to true: show the lead form before any greeting or session. Set false to follow the dashboard capture_mode instead. See Lead capture timing. |
showNewChatButton |
No | Defaults to true. Set false to hide the app-bar refresh / new-chat button. |
Lead capture timing
When lead capture is enabled on the FrontFace dashboard, the Mobile SDK by default
shows the lead form first (requireLeadCaptureBeforeChat: true):
- Lead form
- Form submit creates the session
- API
assembledGreeting(or the dashboard greeting) appears
No local greeting is shown before the form, and no conversation/session is created until the form is submitted.
Dashboard capture_mode still matters when you opt out:
const config = FrontFaceChatConfig(
projectId: '...',
publishableKey: 'pk_...',
requireLeadCaptureBeforeChat: false, // follow dashboard mode
);
email_after— greeting first; form appears after the visitor's first message.email_first/email_required— form before chatting (same as the default).
This only takes effect if lead capture itself is enabled on the dashboard — it changes when the form shows, not whether it's collected at all.
Session expiry: sessionToken expires after 24h of inactivity (expired and
tampered tokens both return 403 SESSION_*). On expiry the SDK clears the chat,
drops the stale session, and shows the lead form again (when lead capture is
enabled). Submitting the form creates a new session and shows the API
assembledGreeting — never a local greeting before the form, and never a
“session expired” error toast. Use FrontFaceChat.debugCorruptSessionToken(projectId)
(or the example app button) to test without waiting 24h.
How it works
Your app
└── FrontFaceChat.open()
└── FrontFaceChatScreen
└── FrontFaceChatProvider
├── FrontFaceApiService
│ └── FrontFaceApiManager → api.frontface.app
└── FrontFaceVisitorStore → SharedPreferences
- Bootstrap — Creates or restores a visitor ID, loads embed config from the API.
- Lead form — Shown when enabled in the dashboard and not yet completed.
- AI chat — Messages sent via
POST /api/chat/message. - Handoff — User can request a human (creating a conversation first via
ensure-conversationif none exists yet); provider polls for agent messages every 2 seconds. - Session — Conversation id and its session token are persisted together so returning users see their history and continued requests stay authorized.
Example app
Run the bundled example, then enter your Project ID and Publishable key in the two fields on the home screen:
cd example
flutter pub get
flutter run
Platform notes
- Android / iOS — Fully supported.
- Markdown links (e.g.
[View Details](https://…)) open in the external browser. Host apps must allow URL queries:- iOS — add
LSApplicationQueriesSchemes:https,http,mailtotoInfo.plist - Android — add
<queries>VIEWintents forhttps/http/mailtoinAndroidManifest.xml
- iOS — add
- Web / desktop — Should work (uses
http+shared_preferences); not primary targets.
Troubleshooting
| Issue | Fix |
|---|---|
| "Chat is currently unavailable" | Widget disabled in FrontFace dashboard or wrong projectId |
| 401 / auth errors | Check publishableKey (pk_…), not the secret key |
| White screen / no messages | Ensure device has network; enable debugLogging: true |
| Lead form keeps showing | Clear app data or call startNewChat after form submit |
License
See LICENSE.
Support
- FrontFace docs: frontface.app
- Package issues: open an issue in this repository