friction_sdk 0.0.4
friction_sdk: ^0.0.4 copied to clipboard
Zero-config friction capture, voice-AI feedback, and auto-ticketing for Flutter apps. The AI knows what broke because it watched it break.
example/lib/main.dart
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:friction_sdk/friction_sdk.dart';
/// Configure this with **your** project's publishable key from the Friction
/// dashboard (Project → Keys). It looks like `pk_live_...` and is safe to ship
/// in client code — it only identifies the project for telemetry ingestion.
const _appId = 'pk_live_YOUR_PROJECT_KEY';
/// Resolves the right host for the local backend depending on platform.
/// Android emulator routes the host's `localhost` to `10.0.2.2`; everywhere else
/// `localhost` (or `127.0.0.1`) works.
String _backendBaseUrl() {
if (kIsWeb) return 'http://localhost:5050';
if (Platform.isAndroid) return 'http://10.0.2.2:5050';
return 'http://localhost:5050';
}
Future<void> main() async {
// Friction.init must run before runApp so error handlers + HttpOverrides
// are installed before the app starts making requests.
await Friction.init(
appId: _appId,
baseUrl: _backendBaseUrl(),
);
runApp(
FrictionScope(
child: MaterialApp(
title: 'Friction example',
debugShowCheckedModeBanner: false,
navigatorObservers: [FrictionRouteObserver()],
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF5B4FFF)),
useMaterial3: true,
),
initialRoute: '/',
routes: {
'/': (_) => const HomePage(),
'/checkout/payment': (_) => const PaymentPage(),
},
),
),
);
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Friction example')),
body: ListView(
padding: const EdgeInsets.all(20),
children: [
Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFEEECFF),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFD9D6FF)),
),
child: Text(
'Backend → ${_backendBaseUrl()}\nAppId → ${_appId.substring(0, 16)}…',
style: const TextStyle(fontSize: 11.5, fontFamily: 'monospace'),
),
),
const Text(
'Tap a tile to trigger a friction signal. The widget will appear from the bottom.',
style: TextStyle(fontSize: 14, color: Colors.black54),
),
const SizedBox(height: 16),
_Tile(
label: '402 from a payment API',
subtitle: 'Triggers `api_error`',
onTap: () => _triggerHttp402(context),
),
_Tile(
label: 'Uncaught exception',
subtitle: 'Triggers `uncaught_exception`',
onTap: () => _triggerException(),
),
_Tile(
label: 'Open the broken payment screen',
subtitle: 'Use the Pay button — it will rage-click',
onTap: () => Navigator.of(context).pushNamed('/checkout/payment'),
),
_Tile(
label: 'Manual report',
subtitle: 'A user-initiated "report a problem"',
onTap: () => Friction.report(
reason: 'manual',
userTranscript: 'I tapped Submit but nothing happened.',
),
),
],
),
);
}
Future<void> _triggerHttp402(BuildContext context) async {
// Hit a URL that returns a non-2xx. httpstat.us is handy for demos.
final client = HttpClient();
try {
final req = await client.getUrl(Uri.parse('https://httpstat.us/402'));
final res = await req.close();
await res.drain<void>();
} catch (_) {
// Even socket errors are friction.
} finally {
client.close();
}
}
void _triggerException() {
Future<void>.microtask(() => throw StateError('demo exception'));
}
}
class PaymentPage extends StatelessWidget {
const PaymentPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Checkout')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Total \$128.40',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.w600)),
const SizedBox(height: 24),
ElevatedButton(
// Intentionally a no-op so users rage-click it.
onPressed: () {},
child: const Text('Pay now'),
),
const SizedBox(height: 12),
const Text(
'Tap "Pay now" rapidly to fire a rage_click trigger.',
style: TextStyle(color: Colors.black54),
),
],
),
),
);
}
}
class _Tile extends StatelessWidget {
const _Tile({required this.label, required this.subtitle, required this.onTap});
final String label;
final String subtitle;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 6),
child: ListTile(
title: Text(label),
subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
),
);
}
}