detour_flutter_plugin 1.0.0 copy "detour_flutter_plugin: ^1.0.0" to clipboard
detour_flutter_plugin: ^1.0.0 copied to clipboard

Flutter bridge SDK for Detour deferred deep links and analytics.

example/lib/main.dart

import 'package:detour_flutter_plugin/detour_flutter_plugin.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

Future<void> main() async {
  try {
    await dotenv.load(fileName: '.env');
  } catch (_) {
    // Local fallback for CI/tests without .env file.
  }
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _detourService = DetourService();

  DetourIntent? _initialIntent;
  DetourIntent? _runtimeIntent;
  DetourIntent? _manualIntent;
  DetourResult? _processedResult;
  String _status = 'Initializing...';

  @override
  void initState() {
    super.initState();
    _detourService.addListener(_onDetourChanged);
    _init();
  }

  Future<void> _init() async {
    try {
      await _detourService.start(
        DetourConfig(
          apiKey: dotenv.env['DETOUR_API_KEY'] ?? 'YOUR_API_KEY',
          appID: dotenv.env['DETOUR_APP_ID'] ?? 'YOUR_APP_ID',
          shouldUseClipboard: true,
          linkProcessingMode: LinkProcessingMode.all,
        ),
      );

      if (mounted &&
          _detourService.isInitialLinkProcessed &&
          _status == 'Initializing...') {
        setState(() => _status = 'Ready');
      }
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() => _status = 'Error: ${e.message}');
    }
  }

  void _onDetourChanged() {
    if (!mounted) return;

    final intent = _detourService.pendingIntent;
    if (intent != null) {
      setState(() {
        switch (intent.source) {
          case DetourIntentSource.initial:
            _initialIntent = intent;
            break;
          case DetourIntentSource.runtime:
            _runtimeIntent = intent;
            break;
          case DetourIntentSource.manual:
            _manualIntent = intent;
            break;
        }
        _status = 'Handled ${intent.source.name} intent';
      });
      _detourService.consumePendingIntent();
      return;
    }

    if (_detourService.isInitialLinkProcessed && _status == 'Initializing...') {
      setState(() => _status = 'Ready');
    }
  }

  @override
  void dispose() {
    _detourService.removeListener(_onDetourChanged);
    _detourService.dispose();
    super.dispose();
  }

  Future<void> _processTestLink() async {
    try {
      final result = await _detourService.processLink(
        // Use a test URL that matches your Detour dashboard setup for testing.
        'https://godetour.link/abc123?campaign=test',
      );
      if (!mounted) return;
      setState(() => _processedResult = result);
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() => _status = 'processLink error: ${e.message}');
    }
  }

  Future<void> _logEvent() async {
    await _detourService.logEvent(
      DetourEventName.purchase,
      data: {'value': 9.99, 'currency': 'USD'},
    );
    if (!mounted) return;
    setState(() => _status = 'logEvent sent');
  }

  Future<void> _logRetention() async {
    await _detourService.logRetention('home_screen_viewed');
    if (!mounted) return;
    setState(() => _status = 'logRetention sent');
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Detour Flutter Plugin')),
        body: SingleChildScrollView(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Status: $_status'),
              const SizedBox(height: 8),
              Text(
                'Initial processed: ${_detourService.isInitialLinkProcessed}',
              ),
              const SizedBox(height: 16),
              _IntentCard('Initial Intent', _initialIntent),
              const SizedBox(height: 8),
              _IntentCard('Runtime Intent', _runtimeIntent),
              const SizedBox(height: 8),
              _IntentCard('Manual Intent', _manualIntent),
              const SizedBox(height: 8),
              _ResultCard('processLink() Result', _processedResult),
              const SizedBox(height: 16),
              Wrap(
                spacing: 8,
                runSpacing: 8,
                children: [
                  ElevatedButton(
                    onPressed: _processTestLink,
                    child: const Text('Process Test Link'),
                  ),
                  ElevatedButton(
                    onPressed: _logEvent,
                    child: const Text('Log Event'),
                  ),
                  ElevatedButton(
                    onPressed: _logRetention,
                    child: const Text('Log Retention'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _IntentCard extends StatelessWidget {
  const _IntentCard(this.title, this.intent);

  final String title;
  final DetourIntent? intent;

  @override
  Widget build(BuildContext context) {
    final value = intent;
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
            if (value == null)
              const Text('None')
            else ...[
              Text('source: ${value.source.name}'),
              Text('type: ${value.link.type.name}'),
              Text('route: ${value.link.route}'),
              Text('pathname: ${value.link.pathname}'),
              Text('params: ${value.link.params}'),
              Text('url: ${value.link.url}'),
            ],
          ],
        ),
      ),
    );
  }
}

class _ResultCard extends StatelessWidget {
  final String title;
  final DetourResult? result;

  const _ResultCard(this.title, this.result);

  @override
  Widget build(BuildContext context) {
    final r = result;
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
            if (r == null)
              const Text('None')
            else if (r.link == null)
              Text('processed: ${r.processed}, link: null')
            else ...[
              Text('type: ${r.link!.type.name}'),
              Text('route: ${r.link!.route}'),
              Text('pathname: ${r.link!.pathname}'),
              Text('params: ${r.link!.params}'),
              Text('url: ${r.link!.url}'),
            ],
          ],
        ),
      ),
    );
  }
}