nusa_chat
In-app customer chat ("NusaChat") for Flutter apps on Android and iOS, connected to a NusaContact Socket Bridge over REST and WebSocket.
Features
- Real-time text chat with history, optimistic sending, retry, HTTP fallback and automatic reconnect.
- Photos from the gallery or an in-app camera, with crop (free, 1:1, 4:5, 16:9, rotate) and drawing (pencil, text, arrow) before sending.
- Documents (PDF, Word, Excel), current location and voice notes.
- Emoji panel with categories, search (Indonesian and English) and recently used emoji.
- Day dividers and a floating date while scrolling; full-screen photo viewer with pinch and double-tap zoom.
- Restyle with
NusaChatTheme/NusaChatStrings, or replace parts with builders.
Usage
NusaChat.open(
context,
page: NusaChatPage(
config: const NusaChatConfig(
baseUrl: 'https://chat-nusa.example.com',
websiteId: 'mynusa',
origin: 'https://nusaselecta.app', // must be in the website's allowed_origins
visitorId: '6281234567890', // optional: your customer id / normalised phone
displayName: 'Budi Santoso', // optional: contact name for the agent
),
onReportTap: () {}, // shows the top-right report button
),
);
Without visitorId the server generates one and the plugin remembers it on the device, so the same visitor
resumes the same conversation.
Platform setup
Each attachment needs its permission in the host app; turn off what you don't use with
attachments: NusaChatAttachmentOptions(...) (or NusaChatAttachmentOptions.none() for text only).
Android (AndroidManifest.xml):
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/> <!-- camera -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <!-- voice notes -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <!-- location -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
Photos use the system photo picker, so no storage permission is needed.
iOS (Info.plist): NSCameraUsageDescription, NSMicrophoneUsageDescription,
NSLocationWhenInUseUsageDescription, and NSPhotoLibraryUsageDescription (iOS 13 only).
Server requirement
origin must be registered in allowed_origins of the websites row for websiteId — otherwise the
session call returns 403 and the socket is closed with 1008. No other backend change is needed.
Customization
| What | How |
|---|---|
| Colours / text styles | theme: NusaChatTheme.fallback().copyWith(myBubbleColor: …, sendButtonGradient: …) |
| Texts | strings: NusaChatStrings(title: 'Bantuan', agentName: 'CS Nusanet', hintText: …) |
| App bar actions | actions: [NusaChatIconButton(icon: Icon(Icons.call), onTap: …)] |
| Back button | leading: (replace) or onBack: (behaviour) |
| Whole app bar | appBarBuilder: (context, data) => AppBar(...) — data has title, connection, onBack |
| Message row | messageBuilder: (context, data, defaultWidget) => … |
| Bubble only | bubbleBuilder: (context, data) => … |
| Agent avatar | agentAvatarBuilder: (context) => NusaChatAvatar(child: Image.asset(...)) |
| Composer | inputBarBuilder: (context, composer) => … (controller, focusNode, send, canSend) |
| Send button | sendButtonBuilder: (context, onSend) => … (onSend is null when empty) |
| Loading / error / empty / connection banner | loadingBuilder, errorBuilder, emptyBuilder, connectionBuilder |
| Notices (e.g. rate limited) | onNotice: (context, message) => … (SnackBar by default) |
Every default widget (NusaChatAppBar, NusaChatBubble, NusaChatMessageRow, NusaChatAvatar,
NusaChatInputBar, NusaChatSendButton, NusaChatIconButton) is exported, so custom builders can reuse them.
They read colours from NusaChatThemeScope.of(context).
Behaviour
- Bootstrap
POST /v1/widget/{website_id}/sessions→ historyGET …/messages→ WebSocket/v1/wswith an explicitOriginheader. - Sent messages appear immediately (clock icon) and turn to a double check once the server saves them (
ack). Acks carry no client id, so they are matched to pending messages in send order. rate_limited/ errors mark the message failed — tap to resend.- When the socket is down, text goes over the HTTP fallback.
- Disconnects reconnect with backoff (1s → 30s) and resync history with
?since=. Close code1008(conversation closed after 24h idle) bootstraps a new conversation once. - The socket is closed while the app is in the background and reopened on resume.
Architecture
Clean architecture:
lib/src/
config/ NusaChatConfig
core/ error (Failure), use_cases (UseCase/StreamUseCase), theme, util/styles (colours, spacing, type)
features/data/ data_sources (remote Dio, socket, local prefs), models (json_serializable), repositories
features/domain/ entities, repositories (contract), use_cases
features/presentation/ bloc/chat, pages/nusa_chat_page.dart, widgets
injection_container.dart own GetIt instance (never GetIt.instance)
Development
flutter pub get
dart run build_runner build
flutter analyze && flutter test
Example app + mock bridge
cd example
dart run tool/mock_bridge.dart # fake bridge on :4000, seeded with a sample conversation
flutter run # emulator/simulator → 10.0.2.2 / 127.0.0.1
flutter run --dart-define=NUSA_CHAT_BASE_URL=http://<mac-lan-ip>:4000 # real device
curl -X POST localhost:4000/agent/<visitor_id> -d '{"text":"Halo dari agent"}' # push an agent reply
Against a real bridge: --dart-define=NUSA_CHAT_BASE_URL=… --dart-define=NUSA_CHAT_WEBSITE_ID=… --dart-define=NUSA_CHAT_ORIGIN=….
License
MIT — see LICENSE. Bundled fonts, icons and emoji data keep their own licenses; see THIRD_PARTY_NOTICES.md.
Libraries
- nusa_chat
- NusaChat — in-app customer chat backed by NusaContact Socket Bridge.