attriax_flutter 0.1.0 copy "attriax_flutter: ^0.1.0" to clipboard
attriax_flutter: ^0.1.0 copied to clipboard

Attriax SDK for Flutter - A powerful and easy-to-use solution for mobile attribution, deep-linking, and analytics.

example/lib/main.dart

import 'dart:async';

import 'package:attriax_flutter/attriax_flutter.dart';
import 'package:flutter/material.dart';

import 'example_app_configuration.dart';
import 'example_attriax_sdk.dart';

const String exampleDefaultAppToken = 'ax_b62ee57056374b76aa09b26fa071e561';

const String exampleAppToken = String.fromEnvironment(
  'ATTRIAX_APP_TOKEN',
  defaultValue: exampleDefaultAppToken,
);

const String exampleApiBaseUrl = String.fromEnvironment('ATTRIAX_API_BASE_URL');

bool isExampleAppConfigured({required String appToken}) =>
    !appToken.startsWith('ax_your_');

void ensureExampleAppConfigured({required String appToken}) {
  if (!isExampleAppConfigured(appToken: appToken)) {
    throw StateError(
      'Replace the example Attriax app token before running this app.',
    );
  }
}

Attriax _createExampleAttriax(ExampleAppConfiguration configuration) {
  ensureExampleAppConfigured(appToken: configuration.appToken);
  final apiBaseUrl = configuration.normalizedApiBaseUrl;

  return Attriax(
    config: apiBaseUrl == null
        ? AttriaxConfig(
            appToken: configuration.appToken,
            sdkMetadata: const <String, Object?>{
              'surface': 'package_example',
              'purpose': 'docs_and_demo',
            },
          )
        : AttriaxConfig(
            appToken: configuration.appToken,
            apiBaseUrl: apiBaseUrl,
            sdkMetadata: const <String, Object?>{
              'surface': 'package_example',
              'purpose': 'docs_and_demo',
            },
          ),
  );
}

LiveExampleAttriaxSdk _createExampleSdk(
  ExampleAppConfiguration configuration,
) => LiveExampleAttriaxSdk(_createExampleAttriax(configuration));

final GlobalKey<NavigatorState> _exampleNavigatorKey =
    GlobalKey<NavigatorState>();

class _ExampleRouteDestination {
  const _ExampleRouteDestination({
    required this.routeName,
    required this.title,
    required this.description,
  });

  final String routeName;
  final String title;
  final String description;
}

class _ExampleDeepLinkPageArgs {
  const _ExampleDeepLinkPageArgs({
    required this.deepLink,
    required this.navigationSource,
    required this.title,
    required this.description,
  });

  final AttriaxDeepLink deepLink;
  final String navigationSource;
  final String title;
  final String description;
}

String _normalizedExampleDeepLinkPath(Uri uri) {
  final candidate = uri.path.isNotEmpty && uri.path != '/'
      ? uri.path
      : uri.host;
  final trimmed = candidate.trim();
  if (trimmed.isEmpty) {
    return '';
  }

  return trimmed
      .replaceFirst(RegExp(r'^/+'), '')
      .replaceFirst(RegExp(r'/+$'), '');
}

AttriaxDeepLink _buildExampleDeepLink(Uri uri, Map<String, String>? data) =>
    AttriaxDeepLink(path: _normalizedExampleDeepLinkPath(uri), data: data);

String _describeExampleDeepLink(Uri? uri) {
  if (uri == null) {
    return 'none';
  }

  final normalizedPath = _normalizedExampleDeepLinkPath(uri);
  return normalizedPath.isEmpty ? uri.toString() : normalizedPath;
}

String _describeExampleResolution(
  Uri? uri,
  AttriaxDeepLinkResolution? resolution,
) {
  if (resolution == null) {
    return 'none';
  }

  if (!resolution.found) {
    return 'external or unmatched';
  }

  return _describeExampleDeepLink(uri);
}

_ExampleRouteDestination _resolveExampleRoute(AttriaxDeepLink deepLink) {
  final segments = deepLink.path
      .split('/')
      .where((segment) => segment.trim().isNotEmpty)
      .toList(growable: false);

  if (segments.isEmpty) {
    return const _ExampleRouteDestination(
      routeName: '/deep-link',
      title: 'Deep Link Screen',
      description: 'Fallback destination for unmatched route patterns.',
    );
  }

  switch (segments.first) {
    case 'promo':
      return const _ExampleRouteDestination(
        routeName: '/promo',
        title: 'Promo Screen',
        description:
            'This simulates opening a campaign or promotion screen from a matched deep link.',
      );
    case 'profile':
      return const _ExampleRouteDestination(
        routeName: '/profile',
        title: 'Profile Screen',
        description:
            'This simulates routing into a user-specific area after deep-link matching succeeds.',
      );
    default:
      return const _ExampleRouteDestination(
        routeName: '/deep-link',
        title: 'Deep Link Screen',
        description: 'Fallback destination for unmatched route patterns.',
      );
  }
}

void _openExampleDeepLink(AttriaxDeepLink deepLink, {required String source}) {
  final navigator = _exampleNavigatorKey.currentState;
  if (navigator == null) {
    return;
  }

  final destination = _resolveExampleRoute(deepLink);
  navigator.pushNamed(
    destination.routeName,
    arguments: _ExampleDeepLinkPageArgs(
      deepLink: deepLink,
      navigationSource: source,
      title: destination.title,
      description: destination.description,
    ),
  );
}

String _maskExampleAppToken(String appToken) {
  if (appToken.length <= 8) {
    return appToken;
  }

  return '${appToken.substring(0, 4)}...${appToken.substring(appToken.length - 4)}';
}

Route<void> _onGenerateExampleRoute(
  RouteSettings settings, {
  required ExampleAttriaxSdk sdk,
  ExampleAppConfiguration? configuration,
  Future<void> Function()? onResetConfiguration,
}) {
  switch (settings.name) {
    case '/':
      return MaterialPageRoute<void>(
        builder: (_) => ExampleHomePage(
          sdk: sdk,
          configuration: configuration,
          onResetConfiguration: onResetConfiguration,
        ),
        settings: settings,
      );
    case '/promo':
    case '/profile':
    case '/deep-link':
      final args = settings.arguments as _ExampleDeepLinkPageArgs?;
      return MaterialPageRoute<void>(
        builder: (_) => _ExampleDeepLinkDestinationPage(args: args),
        settings: settings,
      );
    default:
      return MaterialPageRoute<void>(
        builder: (_) => ExampleHomePage(
          sdk: sdk,
          configuration: configuration,
          onResetConfiguration: onResetConfiguration,
        ),
        settings: settings,
      );
  }
}

// ── Entry point ───────────────────────────────────────────────────────────────

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final configurationStore = ExampleAppConfigurationStore();
  final storedConfiguration = await configurationStore.load();
  final initialConfiguration =
      storedConfiguration ??
      (isExampleAppConfigured(appToken: exampleAppToken)
          ? ExampleAppConfiguration(
              appToken: exampleAppToken,
              apiBaseUrl: exampleApiBaseUrl.isEmpty ? null : exampleApiBaseUrl,
            )
          : null);

  ExampleAttriaxSdk? initialSdk;
  String? initialBootstrapError;
  if (initialConfiguration != null) {
    try {
      initialSdk = _createExampleSdk(initialConfiguration);
      // Recommended: await initialization during startup so the SDK has
      // restored persisted state, collected context, and started listeners
      // before the UI. If your app must remain non-blocking, you can
      // intentionally choose unawaited(attriax.init()) instead.
      await initialSdk.init();
    } catch (error) {
      initialBootstrapError =
          'Saved configuration failed to initialize: ${_formatSetupError(error)}';
      await initialSdk?.dispose();
      initialSdk = null;
    }
  }

  runApp(
    AttriaxPackageExampleApp(
      sdk: initialSdk,
      ownsProvidedSdk: initialSdk != null,
      initialConfiguration: initialConfiguration,
      configurationStore: configurationStore,
      initialBootstrapError: initialBootstrapError,
    ),
  );
}

// ── App widget ────────────────────────────────────────────────────────────────

class AttriaxPackageExampleApp extends StatefulWidget {
  const AttriaxPackageExampleApp({
    super.key,
    this.sdk,
    this.ownsProvidedSdk = false,
    this.initialConfiguration,
    this.configurationStore,
    this.initialBootstrapError,
  });

  final ExampleAttriaxSdk? sdk;
  final bool ownsProvidedSdk;
  final ExampleAppConfiguration? initialConfiguration;
  final ExampleAppConfigurationStore? configurationStore;
  final String? initialBootstrapError;

  @override
  State<AttriaxPackageExampleApp> createState() =>
      _AttriaxPackageExampleAppState();
}

class _AttriaxPackageExampleAppState extends State<AttriaxPackageExampleApp> {
  ExampleAttriaxSdk? _sdk;
  bool _ownsSdk = false;
  bool _isInitializing = false;
  String? _bootstrapError;
  ExampleAppConfiguration? _configuration;

  ExampleAppConfigurationStore get _configurationStore =>
      widget.configurationStore ?? ExampleAppConfigurationStore();

  @override
  void initState() {
    super.initState();
    _sdk = widget.sdk;
    _ownsSdk = widget.ownsProvidedSdk;
    _bootstrapError = widget.initialBootstrapError;
    _configuration = widget.initialConfiguration;
  }

  @override
  void dispose() {
    if (_ownsSdk) {
      unawaited(_sdk?.dispose() ?? Future<void>.value());
    }
    super.dispose();
  }

  Future<void> _applyConfiguration(
    ExampleAppConfiguration configuration,
  ) async {
    setState(() {
      _isInitializing = true;
      _bootstrapError = null;
    });

    LiveExampleAttriaxSdk? nextSdk;
    try {
      nextSdk = _createExampleSdk(configuration);
      await nextSdk.init();
      await _configurationStore.save(configuration);

      final previousSdk = _ownsSdk ? _sdk : null;
      if (!mounted) {
        await nextSdk.dispose();
        return;
      }

      setState(() {
        _sdk = nextSdk;
        _ownsSdk = true;
        _configuration = configuration;
        _isInitializing = false;
      });

      await previousSdk?.dispose();
    } catch (error) {
      await nextSdk?.dispose();
      if (!mounted) {
        return;
      }

      setState(() {
        _bootstrapError = 'Initialization failed: ${_formatSetupError(error)}';
        _isInitializing = false;
      });
    }
  }

  Future<void> _resetConfiguration() async {
    final previousSdk = _ownsSdk ? _sdk : null;
    _exampleNavigatorKey.currentState?.popUntil((route) => route.isFirst);
    await _configurationStore.clear();
    if (!mounted) {
      await previousSdk?.dispose();
      return;
    }

    setState(() {
      _sdk = null;
      _ownsSdk = false;
      _configuration = null;
      _bootstrapError = null;
      _isInitializing = false;
    });

    await previousSdk?.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final sdk = _sdk;

    if (sdk == null) {
      return MaterialApp(
        title: 'Attriax Example',
        theme: ThemeData(
          colorSchemeSeed: const Color(0xFF0F766E),
          useMaterial3: true,
        ),
        home: ExampleSetupPage(
          initialConfiguration: _configuration,
          isInitializing: _isInitializing,
          bootstrapError: _bootstrapError,
          onApplyConfiguration: _applyConfiguration,
        ),
      );
    }

    return MaterialApp(
      title: 'Attriax Example',
      navigatorKey: _exampleNavigatorKey,
      navigatorObservers: sdk.buildNavigatorObservers(),
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF0F766E),
        useMaterial3: true,
      ),
      onGenerateRoute: (settings) => _onGenerateExampleRoute(
        settings,
        sdk: sdk,
        configuration: _configuration,
        onResetConfiguration: _ownsSdk ? _resetConfiguration : null,
      ),
      initialRoute: '/',
    );
  }
}

class ExampleSetupPage extends StatefulWidget {
  const ExampleSetupPage({
    super.key,
    required this.onApplyConfiguration,
    this.initialConfiguration,
    this.isInitializing = false,
    this.bootstrapError,
  });

  final Future<void> Function(ExampleAppConfiguration configuration)
  onApplyConfiguration;
  final ExampleAppConfiguration? initialConfiguration;
  final bool isInitializing;
  final String? bootstrapError;

  @override
  State<ExampleSetupPage> createState() => _ExampleSetupPageState();
}

class _ExampleSetupPageState extends State<ExampleSetupPage> {
  late final TextEditingController _appTokenController = TextEditingController(
    text: widget.initialConfiguration?.appToken ?? exampleAppToken,
  );
  late final TextEditingController _apiBaseUrlController =
      TextEditingController(
        text: widget.initialConfiguration?.displayApiBaseUrl ?? '',
      );
  String? _validationError;

  @override
  void dispose() {
    _appTokenController.dispose();
    _apiBaseUrlController.dispose();
    super.dispose();
  }

  Future<void> _submit() async {
    final appToken = _appTokenController.text.trim();

    try {
      ensureExampleAppConfigured(appToken: appToken);

      final configuration = ExampleAppConfiguration(
        appToken: appToken,
        apiBaseUrl: normalizeExampleApiBaseUrl(_apiBaseUrlController.text),
      );

      setState(() {
        _validationError = null;
      });
      await widget.onApplyConfiguration(configuration);
    } catch (error) {
      setState(() {
        _validationError = _formatSetupError(error);
      });
      return;
    }
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final errorText = _validationError ?? widget.bootstrapError;

    return Scaffold(
      appBar: AppBar(title: const Text('Attriax Example Setup')),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(20),
          child: Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(
                    'Configure the public example',
                    style: theme.textTheme.titleMedium,
                  ),
                  const SizedBox(height: 12),
                  const Text(
                    'This example no longer requires dart-defines or source edits. '
                    'Enter a real Attriax app token on first launch and the app '
                    'will store it locally on this device for later runs.',
                  ),
                  const SizedBox(height: 16),
                  TextField(
                    controller: _appTokenController,
                    enabled: !widget.isInitializing,
                    decoration: const InputDecoration(
                      labelText: 'Attriax app token',
                      helperText:
                          'Required. Replace the placeholder token before initializing.',
                      border: OutlineInputBorder(),
                    ),
                  ),
                  const SizedBox(height: 12),
                  TextField(
                    controller: _apiBaseUrlController,
                    enabled: !widget.isInitializing,
                    decoration: const InputDecoration(
                      labelText: 'API base URL',
                      helperText:
                          'Optional. Leave blank to use the production default.',
                      border: OutlineInputBorder(),
                    ),
                  ),
                  if (errorText != null) ...<Widget>[
                    const SizedBox(height: 12),
                    Text(
                      errorText,
                      style: theme.textTheme.bodyMedium?.copyWith(
                        color: theme.colorScheme.error,
                      ),
                    ),
                  ],
                  const SizedBox(height: 16),
                  FilledButton(
                    onPressed: widget.isInitializing ? null : _submit,
                    child: Text(
                      widget.isInitializing
                          ? 'Initializing SDK...'
                          : 'Save configuration and initialize',
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

String _formatSetupError(Object error) {
  if (error is ArgumentError) {
    return error.message?.toString() ?? error.toString();
  }

  final description = error.toString();
  const badStatePrefix = 'Bad state: ';
  if (description.startsWith(badStatePrefix)) {
    return description.substring(badStatePrefix.length);
  }

  return description;
}

class ExampleHomePage extends StatefulWidget {
  const ExampleHomePage({
    super.key,
    required this.sdk,
    this.configuration,
    this.onResetConfiguration,
  });

  final ExampleAttriaxSdk sdk;
  final ExampleAppConfiguration? configuration;
  final Future<void> Function()? onResetConfiguration;

  @override
  State<ExampleHomePage> createState() => _ExampleHomePageState();
}

class _ExampleHomePageState extends State<ExampleHomePage> {
  final TextEditingController _manualPathController = TextEditingController(
    text: 'promo/spring-launch',
  );
  final TextEditingController _firebaseTokenController = TextEditingController(
    text: 'demo_firebase_registration_token',
  );
  final TextEditingController _apnsTokenController = TextEditingController(
    text: 'demo_apns_device_token',
  );
  StreamSubscription<AttriaxDeepLinkEvent>? _deepLinkSubscription;
  StreamSubscription<AttriaxSynchronizationState>? _synchronizationSubscription;

  bool _sdkEnabled = true;
  bool _eventsEnabled = true;
  String _status = 'SDK initialized.';
  String _firebaseTokenStatus = 'Not sent to Attriax yet.';
  String _apnsTokenStatus = 'Not sent to Attriax yet.';
  AttriaxInstallReferrerDetails? _startupInstallReferrer;
  AttriaxDeepLinkEvent? _startupInitialDeepLink;
  AttriaxDynamicLinkRecord? _lastCreatedDynamicLink;
  Uri? _lastDeepLinkUri;
  AttriaxDeepLinkResolution? _lastResolution;

  @override
  void initState() {
    super.initState();

    _deepLinkSubscription = widget.sdk.deepLinks.stream.listen(
      _handleDeepLinkEvent,
    );
    _synchronizationSubscription = widget.sdk.synchronizationStates.listen((_) {
      if (!mounted) {
        return;
      }
      setState(() {});
    });

    if (widget.sdk.isInitialized) {
      _syncStateFromSdk();
    }
  }

  @override
  void dispose() {
    unawaited(_deepLinkSubscription?.cancel() ?? Future<void>.value());
    unawaited(_synchronizationSubscription?.cancel() ?? Future<void>.value());
    _manualPathController.dispose();
    _firebaseTokenController.dispose();
    _apnsTokenController.dispose();
    super.dispose();
  }

  void _syncStateFromSdk() {
    setState(() {
      _sdkEnabled = widget.sdk.enabled;
      _eventsEnabled = widget.sdk.eventsEnabled;
      _status = widget.sdk.enabled
          ? 'Initialized. Startup attribution is available via referrer and deepLinks.'
          : 'Initialized in disabled mode.';
    });
  }

  Future<void> _handleDeepLinkEvent(AttriaxDeepLinkEvent event) async {
    if (mounted) {
      setState(() {
        _lastDeepLinkUri = event.uri;
        _status = 'Received deep link: ${_describeExampleDeepLink(event.uri)}';
      });
    }

    try {
      final resolution = await event.resolve();
      if (!mounted) {
        return;
      }

      setState(() {
        _lastResolution = resolution;
        _status = resolution.found
            ? 'Matched deep link: ${_describeExampleDeepLink(event.uri)}'
            : 'Recorded external deep link: ${_describeExampleDeepLink(event.uri)}';
      });

      if (resolution.found) {
        _openExampleDeepLink(
          _buildExampleDeepLink(event.uri, resolution.data),
          source: event.isDeferred ? 'deferred_app_open' : 'matched_conversion',
        );
      }
    } catch (error) {
      if (!mounted) {
        return;
      }
      setState(() {
        _status = 'Deep link processing failed: $error';
      });
    }
  }

  Future<void> _loadStartupAttribution() async {
    setState(() => _status = 'Loading startup attribution...');
    try {
      final initialDeepLink = await widget.sdk.deepLinks
          .waitForInitialDeepLink();
      final initialResolution = initialDeepLink == null
          ? null
          : await initialDeepLink.resolve();
      final installReferrer = await widget.sdk.getOriginalInstallReferrer();
      if (!mounted) return;
      setState(() {
        _startupInstallReferrer = installReferrer;
        _startupInitialDeepLink = initialDeepLink;
        _lastDeepLinkUri = initialDeepLink?.uri ?? _lastDeepLinkUri;
        _lastResolution = initialResolution ?? _lastResolution;
        _status = installReferrer == null && initialDeepLink == null
            ? 'Startup attribution loaded. No original install referrer or initial deep link found.'
            : 'Startup attribution loaded.';
      });
    } catch (error) {
      if (!mounted) return;
      setState(() => _status = 'Startup attribution failed: $error');
    }
  }

  Future<void> _recordSampleEvent() async {
    await widget.sdk.recordEvent(
      'purchase_completed',
      eventData: const <String, Object?>{
        'value': 99,
        'currency': 'USD',
        'plan': 'pro',
      },
    );
    if (!mounted) return;
    setState(() => _status = 'Queued purchase_completed event.');
  }

  Future<void> _registerFirebaseToken() async {
    final token = _firebaseTokenController.text.trim();
    await widget.sdk.registerFirebaseMessagingToken(
      token.isEmpty ? null : token,
      metadata: const <String, Object?>{
        'surface': 'package_example',
        'source': 'manual_demo',
      },
    );

    if (!mounted) {
      return;
    }

    setState(() {
      _firebaseTokenStatus = token.isEmpty
          ? 'Firebase token cleared in Attriax.'
          : 'Firebase token sent to Attriax.';
      _status = _firebaseTokenStatus;
    });
  }

  Future<void> _clearFirebaseToken() async {
    _firebaseTokenController.clear();
    await _registerFirebaseToken();
  }

  Future<void> _registerApnsToken() async {
    final token = _apnsTokenController.text.trim();
    await widget.sdk.registerApplePushToken(
      token.isEmpty ? null : token,
      metadata: const <String, Object?>{
        'surface': 'package_example',
        'source': 'manual_demo',
      },
    );

    if (!mounted) {
      return;
    }

    setState(() {
      _apnsTokenStatus = token.isEmpty
          ? 'APNs token cleared in Attriax.'
          : 'APNs token sent to Attriax.';
      _status = _apnsTokenStatus;
    });
  }

  Future<void> _clearApnsToken() async {
    _apnsTokenController.clear();
    await _registerApnsToken();
  }

  Future<void> _setSampleUser() async {
    await widget.sdk.setUser('demo-user-123', userName: 'Package Example User');
    if (!mounted) return;
    setState(() => _status = 'Queued setUser for demo-user-123.');
  }

  Future<void> _createSampleDynamicLink() async {
    final result = await widget.sdk.createDynamicLink(
      name: 'Package example dynamic link',
      destinationUrl: 'https://attriax.com/invite',
      group: 'package-example',
      socialPreview: const AttriaxDynamicLinkSocialPreview(
        title: 'Open the Attriax example app',
        description:
            'Example-generated dynamic link with an attached campaign payload.',
      ),
      data: const <String, Object?>{
        'source': 'flutter_package_example',
        'campaign': 'dynamic-link-demo',
      },
    );

    if (!mounted) return;
    setState(() {
      _lastCreatedDynamicLink = result.link;
      _status = 'Created dynamic link: ${result.link.shortUrl}';
    });
  }

  Future<void> _recordManualDeepLink() async {
    final normalizedPath = _manualPathController.text.trim();
    final manualUri = Uri(
      path: normalizedPath.startsWith('/')
          ? normalizedPath
          : '/$normalizedPath',
    );
    final resolution = await widget.sdk.recordDeepLink(
      linkPath: normalizedPath,
      source: 'package_example_manual',
      metadata: const <String, Object?>{'acceptedBy': 'example_button'},
    );
    if (!mounted) return;
    setState(() {
      _lastDeepLinkUri = manualUri;
      _lastResolution = resolution ?? _lastResolution;
      _status = resolution == null
          ? 'Manual deep-link report sent.'
          : resolution.found
          ? 'Manual deep-link resolution matched ${_describeExampleDeepLink(manualUri)}.'
          : 'Manual deep-link recorded as external or unmatched.';
    });

    if (resolution != null && resolution.found) {
      _openExampleDeepLink(
        _buildExampleDeepLink(manualUri, resolution.data),
        source: 'manual_report',
      );
    }
  }

  void _toggleSdk(bool value) {
    setState(() => _sdkEnabled = value);
    widget.sdk.enabled = value;
    setState(() => _status = 'SDK enabled set to $value.');
  }

  void _toggleEvents(bool value) {
    setState(() => _eventsEnabled = value);
    widget.sdk.eventsEnabled = value;
    setState(() => _status = 'Custom event sending set to $value.');
  }

  void _previewNavigation(String path) {
    _openExampleDeepLink(
      AttriaxDeepLink(
        path: path,
        data: <String, Object?>{'preview': true, 'path': path},
      ),
      source: 'local_preview',
    );

    setState(() {
      _status = 'Previewed app navigation for $path.';
    });
  }

  String _synchronizationLabel(AttriaxSynchronizationState state) {
    switch (state) {
      case AttriaxSynchronizationState.initializing:
        return 'Initializing';
      case AttriaxSynchronizationState.synchronizing:
        return 'Synchronizing';
      case AttriaxSynchronizationState.deferred:
        return 'Deferred';
      case AttriaxSynchronizationState.synchronized:
        return 'Synchronized';
      case AttriaxSynchronizationState.offline:
        return 'Offline';
      case AttriaxSynchronizationState.failed:
        return 'Failed';
      case AttriaxSynchronizationState.disabled:
        return 'Disabled';
    }
  }

  Color _synchronizationColor(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    switch (widget.sdk.synchronizationState) {
      case AttriaxSynchronizationState.synchronized:
        return scheme.primaryContainer;
      case AttriaxSynchronizationState.initializing:
      case AttriaxSynchronizationState.synchronizing:
      case AttriaxSynchronizationState.deferred:
        return scheme.secondaryContainer;
      case AttriaxSynchronizationState.offline:
      case AttriaxSynchronizationState.failed:
        return scheme.errorContainer;
      case AttriaxSynchronizationState.disabled:
        return scheme.surfaceContainerHighest;
    }
  }

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    final synchronizationState = widget.sdk.synchronizationState;
    final configuration = widget.configuration;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Attriax Example'),
        actions: <Widget>[
          if (widget.onResetConfiguration != null)
            IconButton(
              onPressed: widget.onResetConfiguration,
              tooltip: 'Reset saved example configuration',
              icon: const Icon(Icons.tune),
            ),
        ],
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        'Minimal Integration Example',
                        style: textTheme.titleMedium,
                      ),
                      const SizedBox(height: 12),
                      const Text(
                        'This is the public example shipped with the package. '
                        'It demonstrates the recommended awaited init flow, '
                        'SDK synchronization state, deep-link streams, and '
                        'how to navigate to real app screens when a link is matched.',
                      ),
                      const SizedBox(height: 8),
                      Text(
                        'Use the setup screen to save a real app token on '
                        'first launch. The example stores it locally so you '
                        'do not need dart-defines or source edits for each run.',
                        style: textTheme.bodySmall,
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 16),
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('SDK Status', style: textTheme.titleMedium),
                      const SizedBox(height: 12),
                      Container(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 12,
                          vertical: 8,
                        ),
                        decoration: BoxDecoration(
                          color: _synchronizationColor(context),
                          borderRadius: BorderRadius.circular(999),
                        ),
                        child: Text(
                          'Synchronization: ${_synchronizationLabel(synchronizationState)}',
                          style: textTheme.labelLarge,
                        ),
                      ),
                      const SizedBox(height: 12),
                      Text(_status),
                      const SizedBox(height: 8),
                      Text('Initialized: ${widget.sdk.isInitialized}'),
                      Text('Synchronized: ${widget.sdk.isSynchronized}'),
                      Text('SDK enabled: ${widget.sdk.enabled}'),
                      Text('Events enabled: ${widget.sdk.eventsEnabled}'),
                      Text('First launch: ${widget.sdk.isFirstLaunch}'),
                      Text('Device ID: ${widget.sdk.deviceId ?? 'pending…'}'),
                      Text(
                        'Configured app token: ${configuration == null ? 'test or injected SDK' : _maskExampleAppToken(configuration.appToken)}',
                      ),
                      Text(
                        'API base URL: ${configuration?.displayApiBaseUrl ?? 'production default'}',
                      ),
                      const SizedBox(height: 8),
                      Text(
                        'Install referrer campaign: ${_startupInstallReferrer?.campaign ?? 'none'}',
                      ),
                      Text(
                        'Initial deep link: ${_describeExampleDeepLink(_startupInitialDeepLink?.uri)}',
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 16),
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('Router Demo', style: textTheme.titleMedium),
                      const SizedBox(height: 12),
                      const Text(
                        'In real usage, the deepLinks stream decides which screen '
                        'to open. These buttons call the same navigation helper '
                        'used by the SDK listener so you can preview the flow '
                        'without waiting for a backend match.',
                      ),
                      const SizedBox(height: 12),
                      Wrap(
                        spacing: 8,
                        runSpacing: 8,
                        children: <Widget>[
                          FilledButton.tonal(
                            onPressed: () =>
                                _previewNavigation('promo/spring-launch'),
                            child: const Text('Preview promo route'),
                          ),
                          FilledButton.tonal(
                            onPressed: () =>
                                _previewNavigation('profile/demo-user-123'),
                            child: const Text('Preview profile route'),
                          ),
                          FilledButton.tonal(
                            onPressed: () =>
                                _previewNavigation('custom/anything-else'),
                            child: const Text('Preview fallback route'),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 16),
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('Controls', style: textTheme.titleMedium),
                      const SizedBox(height: 12),
                      SwitchListTile.adaptive(
                        contentPadding: EdgeInsets.zero,
                        title: const Text('SDK enabled'),
                        value: _sdkEnabled,
                        onChanged: _toggleSdk,
                      ),
                      SwitchListTile.adaptive(
                        contentPadding: EdgeInsets.zero,
                        title: const Text('Custom event sending enabled'),
                        value: _eventsEnabled,
                        onChanged: _toggleEvents,
                      ),
                      const SizedBox(height: 8),
                      OutlinedButton(
                        onPressed: widget.sdk.isInitialized
                            ? _loadStartupAttribution
                            : null,
                        child: const Text('Load startup attribution result'),
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 16),
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('Actions', style: textTheme.titleMedium),
                      const SizedBox(height: 12),
                      FilledButton.tonal(
                        onPressed: widget.sdk.isInitialized
                            ? _recordSampleEvent
                            : null,
                        child: const Text('Queue purchase_completed event'),
                      ),
                      const SizedBox(height: 8),
                      FilledButton.tonal(
                        onPressed: widget.sdk.isInitialized
                            ? _setSampleUser
                            : null,
                        child: const Text('Queue setUser (demo-user-123)'),
                      ),
                      const SizedBox(height: 8),
                      FilledButton.tonal(
                        onPressed: widget.sdk.isInitialized
                            ? _createSampleDynamicLink
                            : null,
                        child: const Text('Create sample dynamic link'),
                      ),
                      const SizedBox(height: 16),
                      TextField(
                        controller: _manualPathController,
                        decoration: const InputDecoration(
                          labelText: 'Manual deep link path',
                          helperText:
                              'Use when another router handles the incoming link.',
                          border: OutlineInputBorder(),
                        ),
                      ),
                      const SizedBox(height: 8),
                      FilledButton.tonal(
                        onPressed: widget.sdk.isInitialized
                            ? _recordManualDeepLink
                            : null,
                        child: const Text('Report manual deep link'),
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 16),
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        'Uninstall Token Registration',
                        style: textTheme.titleMedium,
                      ),
                      const SizedBox(height: 12),
                      const Text(
                        'Real host apps usually call these methods after '
                        'Firebase Messaging or APNs gives them a token. This '
                        'example lets you manually send a value to Attriax so '
                        'you can verify the integration flow without adding '
                        'Firebase to the demo app.',
                      ),
                      const SizedBox(height: 8),
                      Text(
                        'Android normally sends only the Firebase token. Apple '
                        'platforms can send the Firebase token, the APNs token, '
                        'or both when your app has access to both values.',
                        style: textTheme.bodySmall,
                      ),
                      const SizedBox(height: 16),
                      TextField(
                        controller: _firebaseTokenController,
                        decoration: const InputDecoration(
                          labelText: 'Firebase registration token',
                          helperText:
                              'Example host call: attriax.registerFirebaseMessagingToken(token)',
                          border: OutlineInputBorder(),
                        ),
                      ),
                      const SizedBox(height: 8),
                      Wrap(
                        spacing: 8,
                        runSpacing: 8,
                        children: <Widget>[
                          FilledButton.tonal(
                            onPressed: widget.sdk.isInitialized
                                ? _registerFirebaseToken
                                : null,
                            child: const Text('Send Firebase token'),
                          ),
                          OutlinedButton(
                            onPressed: widget.sdk.isInitialized
                                ? _clearFirebaseToken
                                : null,
                            child: const Text('Clear Firebase token'),
                          ),
                        ],
                      ),
                      const SizedBox(height: 8),
                      Text('Firebase status: $_firebaseTokenStatus'),
                      const SizedBox(height: 16),
                      TextField(
                        controller: _apnsTokenController,
                        decoration: const InputDecoration(
                          labelText: 'Apple APNs device token',
                          helperText:
                              'Example host call: attriax.registerApplePushToken(token)',
                          border: OutlineInputBorder(),
                        ),
                      ),
                      const SizedBox(height: 8),
                      Wrap(
                        spacing: 8,
                        runSpacing: 8,
                        children: <Widget>[
                          FilledButton.tonal(
                            onPressed: widget.sdk.isInitialized
                                ? _registerApnsToken
                                : null,
                            child: const Text('Send APNs token'),
                          ),
                          OutlinedButton(
                            onPressed: widget.sdk.isInitialized
                                ? _clearApnsToken
                                : null,
                            child: const Text('Clear APNs token'),
                          ),
                        ],
                      ),
                      const SizedBox(height: 8),
                      Text('APNs status: $_apnsTokenStatus'),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 16),
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        'Latest Deep Link State',
                        style: textTheme.titleMedium,
                      ),
                      const SizedBox(height: 12),
                      Text(
                        'Last deep link: ${_describeExampleDeepLink(_lastDeepLinkUri)}',
                      ),
                      Text(
                        'Last resolution: ${_describeExampleResolution(_lastDeepLinkUri, _lastResolution)}',
                      ),
                      Text(
                        'Last created short URL: ${_lastCreatedDynamicLink?.shortUrl ?? 'none'}',
                      ),
                      Text(
                        'Last resolution found: ${_lastResolution?.found.toString() ?? 'none'}',
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _ExampleDeepLinkDestinationPage extends StatelessWidget {
  const _ExampleDeepLinkDestinationPage({required this.args});

  final _ExampleDeepLinkPageArgs? args;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final deepLink = args?.deepLink;
    final metadata = deepLink?.data ?? const <String, Object?>{};

    return Scaffold(
      appBar: AppBar(title: Text(args?.title ?? 'Deep Link Screen')),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(20),
          child: Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(
                    args?.title ?? 'Deep Link Screen',
                    style: theme.textTheme.headlineSmall,
                  ),
                  const SizedBox(height: 8),
                  Text(args?.description ?? 'No deep-link payload available.'),
                  const SizedBox(height: 16),
                  Text('Matched path: ${deepLink?.path ?? 'none'}'),
                  Text(
                    'Navigation source: ${args?.navigationSource ?? 'unknown'}',
                  ),
                  const SizedBox(height: 16),
                  Text('Metadata', style: theme.textTheme.titleMedium),
                  const SizedBox(height: 8),
                  if (metadata.isEmpty)
                    const Text('No metadata was attached to this deep link.')
                  else
                    for (final entry in metadata.entries)
                      Padding(
                        padding: const EdgeInsets.only(bottom: 6),
                        child: Text('${entry.key}: ${entry.value}'),
                      ),
                  const Spacer(),
                  FilledButton(
                    onPressed: () => Navigator.of(context).pop(),
                    child: const Text('Back to example home'),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}