instant_request_handler 0.0.1 copy "instant_request_handler: ^0.0.1" to clipboard
instant_request_handler: ^0.0.1 copied to clipboard

PlatformAndroid

Reusable Flutter plugin for immediate incoming request notification experiences.

example/lib/main.dart

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',
      ),
    );
  }
}