brevwick 0.4.1
brevwick: ^0.4.1 copied to clipboard
Brevwick SDK for Flutter — send QA feedback from your app in one widget wrap.
example/lib/main.dart
/// Reference integration of the Brevwick Flutter SDK.
///
/// Exercises every public-surface entry point: `Brevwick.install`,
/// `runGuarded`, `routeObserver`, `dioInterceptor`, the
/// `BrevwickScreenshotScope` + `BrevwickOverlay` widgets, programmatic
/// `captureScreenshot` / `submit`, and ring snapshots.
///
/// Run with the project key + endpoint your tenant provided:
///
/// ```sh
/// flutter run --dart-define=BREVWICK_KEY=pk_test_... \
/// --dart-define=BREVWICK_ENDPOINT=http://localhost:8080
/// ```
library;
import 'package:brevwick/brevwick.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
const String _projectKey = String.fromEnvironment(
'BREVWICK_KEY',
defaultValue: 'pk_test_demoexamplekey00000',
);
const String _endpointRaw = String.fromEnvironment(
'BREVWICK_ENDPOINT',
defaultValue: 'https://api.brevwick.com',
);
void main() {
Brevwick.runGuarded<void>(() async {
WidgetsFlutterBinding.ensureInitialized();
await Brevwick.install(
BrevwickConfig(
projectKey: _projectKey,
endpoint: Uri.parse(_endpointRaw),
environment: 'dev',
release: '0.1.0-example',
user: const BrevwickUser(id: 'demo-user', email: 'demo@brevwick.com'),
userContext: () => const <String, Object?>{
'route': '/example/home',
'feature_flag': 'beta',
},
),
);
runApp(const ExampleApp());
});
}
/// Root of the example app. Wires the SDK's overlay + screenshot scope +
/// route observer, and exposes a Dio configured with the SDK's
/// `BrevwickDioInterceptor` to the home page so failed HTTP calls land in
/// the network ring.
class ExampleApp extends StatefulWidget {
/// Creates an [ExampleApp].
const ExampleApp({super.key});
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
// Long-lived Dio: shared across the home page so the interceptor is
// attached exactly once. Disposed in [dispose] when the example app
// is torn down.
late final Dio _dio = Dio()
..interceptors.add(Brevwick.instance.dioInterceptor());
@override
void dispose() {
_dio.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BrevwickScreenshotScope(
child: MaterialApp(
title: 'Brevwick Example',
navigatorObservers: <NavigatorObserver>[
Brevwick.instance.routeObserver(),
],
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
// Wrap the overlay at the [MaterialApp.builder] level so the FAB
// persists across every named route. Per-route wrapping
// (`/`-only) unmounts the overlay on `Navigator.pushNamed` and
// ships an inconsistent UX — the canonical multi-route shape is
// app-shell-level wrapping.
builder: (BuildContext _, Widget? child) =>
BrevwickOverlay(child: child ?? const SizedBox.shrink()),
initialRoute: '/',
routes: <String, WidgetBuilder>{
'/': (_) => HomePage(dio: _dio),
'/details': (_) => const DetailsPage(),
},
),
);
}
}
/// Home — four buttons, each demonstrating one ring or one programmatic
/// SDK entry point.
class HomePage extends StatelessWidget {
/// Creates a [HomePage] bound to [dio] for the failing-HTTP demo.
const HomePage({required this.dio, super.key});
/// Dio with the Brevwick interceptor installed; used by the
/// "Fail HTTP call" button.
final Dio dio;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Brevwick Example')),
body: Padding(
padding: const EdgeInsets.all(16),
child: ListView(
children: <Widget>[
_DemoButton(
key: const ValueKey<String>('demo-throw-error'),
icon: Icons.error_outline,
label: 'Throw Flutter error',
onPressed: () =>
throw StateError('Demo: simulated Flutter error'),
),
_DemoButton(
key: const ValueKey<String>('demo-fail-http'),
icon: Icons.cloud_off_outlined,
label: 'Fail HTTP call',
onPressed: () => _failHttpCall(context),
),
_DemoButton(
key: const ValueKey<String>('demo-navigate-details'),
icon: Icons.arrow_forward_ios,
label: 'Navigate to details',
onPressed: () => Navigator.of(context).pushNamed('/details'),
),
_DemoButton(
key: const ValueKey<String>('demo-capture-submit'),
icon: Icons.send_outlined,
label: 'Capture + submit',
onPressed: () => _captureAndSubmit(context),
),
],
),
),
);
}
Future<void> _failHttpCall(BuildContext context) async {
final messenger = ScaffoldMessenger.maybeOf(context);
try {
await dio.get<Object?>(
'https://httpbin.org/status/500',
options: Options(validateStatus: (_) => true),
);
} on Object catch (e) {
messenger?.showSnackBar(SnackBar(content: Text('HTTP error: $e')));
return;
}
messenger?.showSnackBar(
const SnackBar(content: Text('Captured 500 into network ring')),
);
}
Future<void> _captureAndSubmit(BuildContext context) async {
// Capture the messenger BEFORE the awaits so we don't need a
// [context.mounted] guard afterwards (the messenger reference
// survives even if the surrounding widget is unmounted).
final messenger = ScaffoldMessenger.maybeOf(context);
final bytes = await Brevwick.instance.captureScreenshot(context);
final attachments = <FeedbackAttachment>[
if (bytes != null)
FeedbackAttachment(
bytes: bytes,
mime: 'image/png',
filename: 'screenshot.png',
),
];
final result = await Brevwick.instance.submit(
FeedbackInput(
title: 'Demo',
description: 'Programmatic capture + submit',
attachments: attachments,
),
);
switch (result) {
case SubmitOk(:final issueId):
messenger?.showSnackBar(SnackBar(content: Text('Submitted: $issueId')));
case SubmitFailed(:final code, :final message):
messenger?.showSnackBar(
SnackBar(content: Text('${code.wireName}: $message')),
);
}
}
}
class _DemoButton extends StatelessWidget {
const _DemoButton({
required this.icon,
required this.label,
required this.onPressed,
super.key,
});
final IconData icon;
final String label;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: FilledButton.icon(
onPressed: onPressed,
icon: Icon(icon),
label: Text(label),
),
);
}
}
/// Visual ring inspector — reads the current SDK ring snapshots and lays
/// them out as collapsible sections so a manual smoke run can confirm
/// each ring populated.
class DetailsPage extends StatefulWidget {
/// Creates a [DetailsPage].
const DetailsPage({super.key});
@override
State<DetailsPage> createState() => _DetailsPageState();
}
class _DetailsPageState extends State<DetailsPage> {
// Ring snapshots are read in [initState] (NOT at field-declaration
// time): if a copy-paster calls `runApp` before `Brevwick.install`,
// touching `Brevwick.instance` during widget-construction would crash
// with a StateError. Reading in [initState] keeps the failure surface
// localised to the route push and easier to diagnose.
late List<Map<String, Object?>> _console;
late List<Map<String, Object?>> _network;
late List<Map<String, Object?>> _route;
@override
void initState() {
super.initState();
_console = Brevwick.instance.snapshotConsole();
_network = Brevwick.instance.snapshotNetwork();
_route = Brevwick.instance.snapshotRoute();
}
void _refresh() {
setState(() {
_console = Brevwick.instance.snapshotConsole();
_network = Brevwick.instance.snapshotNetwork();
_route = Brevwick.instance.snapshotRoute();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Ring snapshots'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Refresh',
onPressed: _refresh,
),
],
),
body: ListView(
padding: const EdgeInsets.all(12),
children: <Widget>[
_RingSection(
title: 'Console (${_console.length})',
entries: _console,
),
_RingSection(
title: 'Network (${_network.length})',
entries: _network,
),
_RingSection(title: 'Route (${_route.length})', entries: _route),
],
),
);
}
}
class _RingSection extends StatelessWidget {
const _RingSection({required this.title, required this.entries});
final String title;
final List<Map<String, Object?>> entries;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 6),
child: ExpansionTile(
title: Text(title),
children: <Widget>[
if (entries.isEmpty)
const Padding(padding: EdgeInsets.all(12), child: Text('Empty'))
else
for (final entry in entries)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
child: Align(
alignment: Alignment.centerLeft,
child: Text(entry.toString()),
),
),
],
),
);
}
}