clariodesk 0.1.2
clariodesk: ^0.1.2 copied to clipboard
ClarioDesk Flutter SDK — in-app customer support chat, bug reporting with auto screenshots, and feedback for mobile apps. Prebuilt UI or headless API.
import 'dart:io' show Platform;
import 'package:clariodesk/clariodesk.dart';
import 'package:clariodesk/widgets.dart';
import 'package:cupertino_http/cupertino_http.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'inbox_screen.dart';
import 'new_conversation_screen.dart';
// API key is injected at build time via --dart-define so it never lands in
// source. See README.md for the run command. The default base URL points at
// the production API on clariodesk.com so the example works against the real
// dashboard you ship.
const _apiKey = String.fromEnvironment('CLARIODESK_API_KEY');
const _baseUrl = String.fromEnvironment(
'CLARIODESK_BASE_URL',
defaultValue: 'https://api.clariodesk.com',
);
// On Apple platforms, dart:io HttpClient sometimes hangs against HTTP/2
// origins (Cloudflare/Fly's edge) under iOS simulators. NSURLSession (what
// Safari uses) doesn't have this problem, so we route the SDK through
// cupertino_http on iOS/macOS. Android keeps the default IOClient.
http.Client? _platformHttpClient() {
if (kIsWeb) return null;
if (Platform.isIOS || Platform.isMacOS) {
return CupertinoClient.fromSessionConfiguration(
URLSessionConfiguration.defaultSessionConfiguration(),
);
}
return null;
}
Future<void> main() async {
if (_apiKey.isEmpty) {
runApp(const _MissingKeyApp());
return;
}
// init() is async on the device-signed protocol — the first launch
// generates the hardware key + registers with the backend. Awaiting is
// optional (subsequent calls auto-await the in-flight bootstrap) but the
// example does so to surface registration failures up front.
await ClarioDesk.init(
apiKey: _apiKey,
baseUrl: Uri.parse(_baseUrl),
httpClient: _platformHttpClient(),
);
runApp(const ClarioDeskExampleApp());
}
class ClarioDeskExampleApp extends StatelessWidget {
const ClarioDeskExampleApp({super.key});
@override
Widget build(BuildContext context) {
final scheme = ColorScheme.fromSeed(seedColor: const Color(0xFFD7FF3D));
return MaterialApp(
title: 'ClarioDesk Example',
theme: ThemeData(colorScheme: scheme, useMaterial3: true),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFFD7FF3D),
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const _RootScreen(),
);
}
}
// Inbox is the home screen — the Device-Is-Identity protocol makes the SDK
// fully online after `init()` (no login required). The appbar exposes the
// optional flows:
//
// • "New" / "Bug" — start a conversation or file a bug.
// • "Pre-built UI" — opens the drop-in ClarioDeskWidgets inbox over the same
// headless SDK these screens drive by hand (for side-by-side comparison).
// • "Identify" — attach an externalId/email label to the device. Pure
// metadata; the dashboard agent sees who they're talking to. Optional.
// • "Reset device" — best-effort revokes server-side, wipes the local
// hardware key, then re-bootstraps a fresh device. Useful for testing the
// registration flow. The `_session` counter keys the inbox so it
// re-subscribes (and re-primes) after the wipe.
class _RootScreen extends StatefulWidget {
const _RootScreen();
@override
State<_RootScreen> createState() => _RootScreenState();
}
class _RootScreenState extends State<_RootScreen> {
bool _resetting = false;
int _session = 0;
Future<void> _showIdentifyForm() async {
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (_) => Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: const _IdentifyForm(),
),
);
if (mounted) setState(() {}); // refresh "identified" badge in appbar
}
Future<void> _resetAndRebootstrap() async {
setState(() => _resetting = true);
try {
await ClarioDesk.reset();
await ClarioDesk.init(
apiKey: _apiKey,
baseUrl: Uri.parse(_baseUrl),
httpClient: _platformHttpClient(),
);
if (mounted) setState(() => _session++);
} finally {
if (mounted) setState(() => _resetting = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ClarioDesk Example'),
actions: [
IconButton(
tooltip: 'New conversation',
icon: const Icon(Icons.add),
onPressed: _resetting
? null
: () => Navigator.of(context).push(MaterialPageRoute<void>(
builder: (_) => const NewConversationScreen(),
)),
),
IconButton(
tooltip: 'Report a bug',
icon: const Icon(Icons.bug_report_outlined),
onPressed: _resetting
? null
: () => Navigator.of(context).push(MaterialPageRoute<void>(
builder: (_) => const BugReportScreen(),
)),
),
PopupMenuButton<String>(
tooltip: 'More',
onSelected: (v) {
switch (v) {
case 'prebuilt':
ClarioDeskWidgets.openInbox(context);
case 'identify':
_showIdentifyForm();
case 'reset':
_resetAndRebootstrap();
}
},
itemBuilder: (_) => [
const PopupMenuItem(
value: 'prebuilt',
child: Text('Open pre-built UI'),
),
PopupMenuItem(
value: 'identify',
child: Text(ClarioDesk.isIdentified
? 'Update user label'
: 'Identify (optional)'),
),
const PopupMenuItem(
value: 'reset',
child: Text('Reset device'),
),
],
),
],
),
body: InboxScreen(key: ValueKey(_session)),
);
}
}
class _IdentifyForm extends StatefulWidget {
const _IdentifyForm();
@override
State<_IdentifyForm> createState() => _IdentifyFormState();
}
class _IdentifyFormState extends State<_IdentifyForm> {
final _externalId = TextEditingController(text: 'demo-user-1');
final _email = TextEditingController(text: 'demo@example.com');
bool _busy = false;
String? _error;
@override
void dispose() {
_externalId.dispose();
_email.dispose();
super.dispose();
}
Future<void> _submit() async {
setState(() {
_busy = true;
_error = null;
});
try {
await ClarioDesk.identify(
externalId: _externalId.text.trim(),
email: _email.text.trim().isEmpty ? null : _email.text.trim(),
).timeout(
const Duration(seconds: 10),
onTimeout: () => throw Exception(
'identify timed out after 10s — check network / API URL'),
);
if (mounted) Navigator.of(context).pop();
} catch (e) {
setState(() => _error = e.toString());
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Tell us who you are',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
// Demo-facing copy: end-user-friendly because the example is a
// reference customers might paste into their own app. The technical
// reality (device-bound key, label is pure metadata) lives in the
// SDK README, not in front of a person filing a bug.
Text(
'Optional. Helps the support team recognize you across tickets '
'and reply with context. You can file tickets either way.',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
TextField(
controller: _externalId,
decoration: const InputDecoration(
labelText: 'External user id',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _email,
decoration: const InputDecoration(
labelText: 'Email (optional)',
border: OutlineInputBorder(),
),
),
if (_error != null) ...[
const SizedBox(height: 12),
Text(_error!, style: const TextStyle(color: Colors.red)),
],
const SizedBox(height: 16),
FilledButton(
onPressed: _busy ? null : _submit,
child: Text(_busy ? 'Identifying…' : 'Identify'),
),
],
),
),
);
}
}
class _MissingKeyApp extends StatelessWidget {
const _MissingKeyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.key_off, size: 48),
const SizedBox(height: 16),
Text('Missing CLARIODESK_API_KEY',
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
const Text('Pass your key at run time:',
textAlign: TextAlign.center),
const SizedBox(height: 8),
const SelectableText(
'flutter run --dart-define=CLARIODESK_API_KEY=pk_live_…',
style: TextStyle(fontFamily: 'monospace'),
),
],
),
),
),
),
);
}
}