daybook_feedback 0.1.0
daybook_feedback: ^0.1.0 copied to clipboard
In-app feedback for Flutter: shake, screenshot, a button or a two-finger long press opens a composer with a screenshot you can pin. Only for people approved in Daybook.
example/lib/main.dart
import 'package:daybook_feedback/daybook_feedback.dart';
import 'package:flutter/material.dart';
/// A small shop with Daybook feedback in it.
///
/// Run against your own project:
/// flutter run --dart-define=DAYBOOK_KEY=pmi_... --dart-define=DAYBOOK_EMAIL=you@example.com
/// and, for a local API, --dart-define=DAYBOOK_API=http://localhost:5099
///
/// The first run lands in the project's Reporters queue as Pending and shows nothing. Approve the
/// request in Daybook, restart, and the triggers the key has on start working.
const _key = String.fromEnvironment('DAYBOOK_KEY');
const _api = String.fromEnvironment('DAYBOOK_API', defaultValue: kDefaultApiUrl);
const _email = String.fromEnvironment('DAYBOOK_EMAIL', defaultValue: 'tester@example.com');
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await startFeedback();
runApp(DaybookFeedback.wrap(const ShopApp()));
}
/// Kept separate so the integration test can start the same way.
Future<void> startFeedback() async {
await DaybookFeedback.init(ingestKey: _key, apiUrl: _api, appVersion: '1.0.0');
// In a real app: once you know who is signed in.
await DaybookFeedback.identify(FeedbackIdentity(externalId: 'example:$_email', email: _email, name: 'Example tester'));
}
class ShopApp extends StatelessWidget {
const ShopApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Daybook feedback example',
navigatorObservers: [DaybookFeedback.navigatorObserver],
initialRoute: '/cart',
routes: {
'/cart': (_) => const CartPage(),
'/profile': (_) => const ProfilePage(),
},
);
}
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Cart'), actions: [
IconButton(
icon: const Icon(Icons.person),
onPressed: () => Navigator.of(context).pushNamed('/profile'),
),
]),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const ListTile(title: Text('Blue mug'), trailing: Text('€12.00')),
const ListTile(title: Text('Notebook'), trailing: Text('€8.50')),
const Divider(),
const ListTile(title: Text('Total'), trailing: Text('€20.50', key: ValueKey('total'))),
const SizedBox(height: 16),
KeyedSubtree(
key: const ValueKey('pay-button'),
child: FilledButton(onPressed: () {}, child: const Text('Pay now')),
),
const SizedBox(height: 32),
const _FeedbackStatus(),
],
),
);
}
/// What the SDK thinks, for this demo. A real app would not show this to its users.
class _FeedbackStatus extends StatelessWidget {
const _FeedbackStatus();
@override
Widget build(BuildContext context) => ValueListenableBuilder<HandshakeResult?>(
valueListenable: DaybookFeedback.state,
builder: (context, state, _) {
final text = switch (state?.status) {
null => 'Feedback: not connected',
ReporterStatus.approved => 'Feedback: on (${state!.triggers.map((t) => t.wire).join(', ')})',
ReporterStatus.pending => 'Feedback: waiting for approval in Daybook',
ReporterStatus.blocked => 'Feedback: blocked',
ReporterStatus.unknown => 'Feedback: unknown',
};
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
Text(text, key: const ValueKey('feedback-status'), style: Theme.of(context).textTheme.bodySmall),
if (state?.isActive ?? false)
TextButton(
key: const ValueKey('open-feedback'),
onPressed: () => DaybookFeedback.open(),
child: const Text('Send feedback'),
),
]);
},
);
}
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: const Center(child: Text('Streak: 40 days')),
);
}