instant_request_handler 0.0.1
instant_request_handler: ^0.0.1 copied to clipboard
Reusable Flutter plugin for immediate incoming request notification experiences.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:instant_request_handler/instant_request_handler.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Firebase setup placeholder:
// await Firebase.initializeApp(
// options: DefaultFirebaseOptions.currentPlatform,
// );
// FirebaseMessaging.onBackgroundMessage(instantRequestBackgroundHandler);
// FirebaseMessaging.onMessage.listen(
// InstantRequestHandler.handleForegroundMessage,
// );
await InstantRequestHandler.initialize(
notificationTypes: <InstantNotificationType<dynamic>>[
InstantNotificationType<FakeRideDetails>(
action: 'new_ride_request',
type: 'new_ride_request',
idKey: 'ride_request_id',
timeoutSeconds: 30,
presentation: InstantRequestPresentation.fullScreenOrSheet,
onFetchData: (String requestId, Map<String, dynamic> payload) async {
await Future<void>.delayed(const Duration(milliseconds: 900));
return FakeRideDetails(
passengerName: 'Sample passenger',
pickup: payload['pickup']?.toString() ?? 'Downtown',
destination: payload['destination']?.toString() ?? 'Airport',
fare: '\$18.40',
distance: '6.2 mi',
);
},
onAccept: (String requestId, FakeRideDetails? data) async {
debugPrint('Accepted request $requestId for ${data?.passengerName}');
await Future<void>.delayed(const Duration(milliseconds: 350));
},
onReject:
(
String requestId,
FakeRideDetails? data,
InstantRequestCloseReason reason,
) async {
debugPrint('Rejected request $requestId with reason $reason');
await Future<void>.delayed(const Duration(milliseconds: 200));
},
builder:
(BuildContext context, InstantRequestState<FakeRideDetails> state) {
return RideRequestSheet(state: state);
},
),
],
);
runApp(const ExampleApp());
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: InstantRequestHandler.navigatorKey,
title: 'Instant Request Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF276EF1)),
useMaterial3: true,
),
home: const ExampleHome(),
);
}
}
class ExampleHome extends StatefulWidget {
const ExampleHome({super.key});
@override
State<ExampleHome> createState() => _ExampleHomeState();
}
class _ExampleHomeState extends State<ExampleHome> {
InstantRequestPermissionStatus? _permissionStatus;
@override
void initState() {
super.initState();
unawaited(_loadPermissions());
}
Future<void> _loadPermissions() async {
final status = await InstantRequestHandler.checkPermissions();
if (!mounted) {
return;
}
setState(() {
_permissionStatus = status;
});
}
Future<void> _simulatePayload() async {
final id = DateTime.now().millisecondsSinceEpoch.toString();
await InstantRequestHandler.handlePayload(<String, dynamic>{
'action': 'new_ride_request',
'type': 'new_ride_request',
'ride_request_id': id,
'pickup': 'Market Street',
'destination': 'Terminal 2',
});
}
@override
Widget build(BuildContext context) {
final status = _permissionStatus;
return Scaffold(
appBar: AppBar(title: const Text('Instant request demo')),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
FilledButton.icon(
onPressed: _simulatePayload,
icon: const Icon(Icons.notifications_active),
label: const Text('Simulate incoming payload'),
),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => const InstantRequestPermissionOnboarding(),
),
);
},
icon: const Icon(Icons.settings),
label: const Text('Open permission setup'),
),
const SizedBox(height: 24),
Text(
'Permission status',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
_StatusLine(
label: 'Notifications',
enabled: status?.notificationsEnabled,
),
_StatusLine(
label: 'Full-screen intent',
enabled: status?.fullScreenIntentEnabled,
),
_StatusLine(label: 'Overlay', enabled: status?.overlayEnabled),
_StatusLine(
label: 'Battery optimization ignored',
enabled: status?.batteryOptimizationIgnored,
),
const SizedBox(height: 12),
TextButton.icon(
onPressed: _loadPermissions,
icon: const Icon(Icons.refresh),
label: const Text('Refresh'),
),
],
),
),
);
}
}
class RideRequestSheet extends StatelessWidget {
const RideRequestSheet({super.key, required this.state});
final InstantRequestState<FakeRideDetails> state;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final data = state.data;
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(16),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Material(
elevation: 10,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
color: theme.colorScheme.surface,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: Text(
'Ride request',
style: theme.textTheme.headlineSmall,
),
),
Text(
'${state.secondsRemaining}s',
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.primary,
),
),
],
),
const SizedBox(height: 16),
if (state.isLoading) ...<Widget>[
const LinearProgressIndicator(),
const SizedBox(height: 12),
Text(
'Fetching request details...',
style: theme.textTheme.bodyMedium,
),
] else if (data != null) ...<Widget>[
_RouteRow(icon: Icons.person, text: data.passengerName),
_RouteRow(icon: Icons.trip_origin, text: data.pickup),
_RouteRow(icon: Icons.flag, text: data.destination),
const SizedBox(height: 12),
Wrap(
spacing: 16,
runSpacing: 8,
children: <Widget>[
_Metric(label: 'Fare', value: data.fare),
_Metric(label: 'Distance', value: data.distance),
],
),
],
if (state.error != null) ...<Widget>[
const SizedBox(height: 12),
Text(
state.error!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.error,
),
),
],
const SizedBox(height: 20),
Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.end,
children: <Widget>[
if (state.error != null)
OutlinedButton.icon(
onPressed: state.isSubmitting ? null : state.retry,
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
),
OutlinedButton.icon(
onPressed: state.isSubmitting ? null : state.reject,
icon: const Icon(Icons.close),
label: const Text('Reject'),
),
FilledButton.icon(
onPressed: state.isSubmitting ? null : state.accept,
icon: const Icon(Icons.check),
label: const Text('Accept'),
),
],
),
],
),
),
),
),
),
);
}
}
class FakeRideDetails {
const FakeRideDetails({
required this.passengerName,
required this.pickup,
required this.destination,
required this.fare,
required this.distance,
});
final String passengerName;
final String pickup;
final String destination;
final String fare;
final String distance;
}
class _RouteRow extends StatelessWidget {
const _RouteRow({required this.icon, required this.text});
final IconData icon;
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: <Widget>[
Icon(icon, size: 20),
const SizedBox(width: 10),
Expanded(child: Text(text)),
],
),
);
}
}
class _Metric extends StatelessWidget {
const _Metric({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(label, style: theme.textTheme.labelMedium),
Text(value, style: theme.textTheme.titleMedium),
],
);
}
}
class _StatusLine extends StatelessWidget {
const _StatusLine({required this.label, required this.enabled});
final String label;
final bool? enabled;
@override
Widget build(BuildContext context) {
final value = enabled;
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(
value == true ? Icons.check_circle : Icons.info_outline,
color: value == true ? Colors.green.shade700 : null,
),
title: Text(label),
trailing: Text(
value == null
? 'Unknown'
: value
? 'Enabled'
: 'Off',
),
);
}
}