adopture 0.1.1 copy "adopture: ^0.1.1" to clipboard
adopture: ^0.1.1 copied to clipboard

Privacy-first mobile analytics SDK for Flutter. Offline-first event tracking, automatic sessions, revenue analytics, and privacy-preserving hashed IDs.

example/lib/main.dart

import 'dart:async';

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

/// Change this to your actual app key for real testing.
const _testAppKey = 'ak_OLTmMJbR2CWQAGsp21IXBP9F';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Adopture.init(
    appKey: _testAppKey,
    debug: true,
    autoCapture: true,
    flushInterval: const Duration(seconds: 10),
    flushAt: 5,
    maxQueueSize: 500,
  );

  Adopture.identify('test-user-001');

  runApp(const TestApp());
}

// ---------------------------------------------------------------------------
// App Root
// ---------------------------------------------------------------------------

class TestApp extends StatelessWidget {
  const TestApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SDK Test App',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.indigo,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const HomeScreen(),
    );
  }
}

// ---------------------------------------------------------------------------
// Home Screen — Dashboard with SDK state + navigation
// ---------------------------------------------------------------------------

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  Timer? _refreshTimer;
  final List<String> _eventLog = [];

  @override
  void initState() {
    super.initState();
    Adopture.screen('HomeScreen');
    _refreshTimer = Timer.periodic(
      const Duration(seconds: 1),
      (_) => setState(() {}),
    );
  }

  @override
  void dispose() {
    _refreshTimer?.cancel();
    super.dispose();
  }

  void _log(String message) {
    setState(() {
      _eventLog.insert(0, '${_timestamp()} $message');
      if (_eventLog.length > 50) _eventLog.removeLast();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('SDK Test App'),
        actions: [
          IconButton(
            icon: const Icon(Icons.delete_outline),
            tooltip: 'Clear log',
            onPressed: () => setState(() => _eventLog.clear()),
          ),
        ],
      ),
      body: Column(
        children: [
          // SDK State Card
          _SdkStateCard(),

          const Divider(height: 1),

          // Navigation to test screens
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                _NavChip(
                  label: 'Profile',
                  icon: Icons.person,
                  onTap: () => _navigateTo(context, const ProfileScreen()),
                ),
                _NavChip(
                  label: 'Settings',
                  icon: Icons.settings,
                  onTap: () => _navigateTo(context, const SettingsScreen()),
                ),
                _NavChip(
                  label: 'Shop',
                  icon: Icons.shopping_cart,
                  onTap: () => _navigateTo(context, const ShopScreen()),
                ),
                _NavChip(
                  label: 'Revenue',
                  icon: Icons.attach_money,
                  onTap: () => _navigateTo(context, const RevenueDemoScreen()),
                ),
                _NavChip(
                  label: 'Stress Test',
                  icon: Icons.speed,
                  onTap: () =>
                      _navigateTo(context, StressTestScreen(onLog: _log)),
                ),
              ],
            ),
          ),

          const Divider(height: 1),

          // Action Buttons
          Padding(
            padding: const EdgeInsets.all(16),
            child: Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                _ActionButton(
                  label: 'Track Event',
                  icon: Icons.touch_app,
                  onPressed: () {
                    Adopture.track('button_clicked', {
                      'screen': 'home',
                      'variant': 'primary',
                    });
                    _log('track: button_clicked');
                  },
                ),
                _ActionButton(
                  label: 'Flush',
                  icon: Icons.send,
                  onPressed: () async {
                    await Adopture.flush();
                    _log('flush: manual');
                  },
                ),
                _ActionButton(
                  label: 'Reset',
                  icon: Icons.restart_alt,
                  color: Colors.orange,
                  onPressed: () async {
                    await Adopture.reset();
                    _log('reset: cleared queue + new session');
                  },
                ),
                _ActionButton(
                  label: Adopture.isEnabled ? 'Disable' : 'Enable',
                  icon: Adopture.isEnabled
                      ? Icons.pause_circle
                      : Icons.play_circle,
                  color: Adopture.isEnabled ? Colors.red : Colors.green,
                  onPressed: () async {
                    if (Adopture.isEnabled) {
                      await Adopture.disable();
                      _log('tracking: DISABLED (opt-out)');
                    } else {
                      Adopture.enable();
                      _log('tracking: ENABLED');
                    }
                  },
                ),
              ],
            ),
          ),

          const Divider(height: 1),

          // Event Log
          Expanded(
            child: _eventLog.isEmpty
                ? const Center(
                    child: Text(
                      'No events yet.\nTap buttons or navigate screens.',
                      textAlign: TextAlign.center,
                      style: TextStyle(color: Colors.grey),
                    ),
                  )
                : ListView.builder(
                    padding: const EdgeInsets.all(8),
                    itemCount: _eventLog.length,
                    itemBuilder: (_, i) => Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Text(
                        _eventLog[i],
                        style: const TextStyle(
                          fontFamily: 'monospace',
                          fontSize: 12,
                        ),
                      ),
                    ),
                  ),
          ),
        ],
      ),
    );
  }

  void _navigateTo(BuildContext context, Widget screen) {
    Navigator.of(context).push(
      MaterialPageRoute(builder: (_) => screen),
    );
  }
}

// ---------------------------------------------------------------------------
// Profile Screen
// ---------------------------------------------------------------------------

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

  @override
  State<ProfileScreen> createState() => _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  @override
  void initState() {
    super.initState();
    Adopture.screen('ProfileScreen');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Profile')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          const CircleAvatar(radius: 40, child: Icon(Icons.person, size: 40)),
          const SizedBox(height: 16),
          const Text(
            'Test User',
            textAlign: TextAlign.center,
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'test-user-001',
            textAlign: TextAlign.center,
            style: TextStyle(color: Colors.grey),
          ),
          const SizedBox(height: 32),
          ListTile(
            leading: const Icon(Icons.edit),
            title: const Text('Edit Profile'),
            onTap: () {
              Adopture.track('profile_edit_tapped');
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Tracked: profile_edit_tapped')),
              );
            },
          ),
          ListTile(
            leading: const Icon(Icons.photo),
            title: const Text('Change Avatar'),
            onTap: () {
              Adopture.track('avatar_change_tapped');
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Tracked: avatar_change_tapped')),
              );
            },
          ),
          ListTile(
            leading: const Icon(Icons.logout),
            title: const Text('Logout'),
            onTap: () {
              Adopture.track('logout_tapped');
              Adopture.reset();
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Tracked: logout + reset')),
              );
            },
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Settings Screen
// ---------------------------------------------------------------------------

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

  @override
  State<SettingsScreen> createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> {
  bool _notifications = true;
  bool _darkMode = true;

  @override
  void initState() {
    super.initState();
    Adopture.screen('SettingsScreen');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Settings')),
      body: ListView(
        children: [
          SwitchListTile(
            title: const Text('Push Notifications'),
            subtitle: const Text('Tracks: setting_changed'),
            value: _notifications,
            onChanged: (v) {
              setState(() => _notifications = v);
              Adopture.track('setting_changed', {
                'setting': 'notifications',
                'value': v.toString(),
              });
            },
          ),
          SwitchListTile(
            title: const Text('Dark Mode'),
            subtitle: const Text('Tracks: setting_changed'),
            value: _darkMode,
            onChanged: (v) {
              setState(() => _darkMode = v);
              Adopture.track('setting_changed', {
                'setting': 'dark_mode',
                'value': v.toString(),
              });
            },
          ),
          const Divider(),
          ListTile(
            leading: const Icon(Icons.privacy_tip),
            title: const Text('Opt-out of Analytics'),
            subtitle: const Text('Calls Adopture.disable()'),
            trailing: const Icon(Icons.chevron_right),
            onTap: () async {
              await Adopture.disable();
              if (context.mounted) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(
                    content: Text('Analytics disabled. Queue cleared.'),
                  ),
                );
              }
            },
          ),
          ListTile(
            leading: const Icon(Icons.analytics),
            title: const Text('Opt back in'),
            subtitle: const Text('Calls Adopture.enable()'),
            trailing: const Icon(Icons.chevron_right),
            onTap: () {
              Adopture.enable();
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Analytics re-enabled.')),
              );
            },
          ),
          const Divider(),
          const _SdkInfoTile(),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Shop Screen — simulates e-commerce events
// ---------------------------------------------------------------------------

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

  @override
  State<ShopScreen> createState() => _ShopScreenState();
}

class _ShopScreenState extends State<ShopScreen> {
  static const _products = [
    ('Wireless Headphones', '79.99', 'electronics'),
    ('Running Shoes', '129.00', 'sports'),
    ('Coffee Beans 1kg', '24.50', 'food'),
    ('Flutter Book', '39.99', 'books'),
    ('USB-C Hub', '49.99', 'electronics'),
  ];

  @override
  void initState() {
    super.initState();
    Adopture.screen('ShopScreen');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Shop')),
      body: ListView.builder(
        padding: const EdgeInsets.all(8),
        itemCount: _products.length,
        itemBuilder: (context, i) {
          final (name, price, category) = _products[i];
          return Card(
            child: ListTile(
              leading: const Icon(Icons.shopping_bag),
              title: Text(name),
              subtitle: Text('\$$price  ·  $category'),
              trailing: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  IconButton(
                    icon: const Icon(Icons.visibility),
                    tooltip: 'View product',
                    onPressed: () {
                      Adopture.track('product_viewed', {
                        'product': name,
                        'price': price,
                        'category': category,
                      });
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(
                            content: Text('Tracked: product_viewed ($name)')),
                      );
                    },
                  ),
                  IconButton(
                    icon: const Icon(Icons.add_shopping_cart),
                    tooltip: 'Add to cart',
                    onPressed: () {
                      Adopture.track('add_to_cart', {
                        'product': name,
                        'price': price,
                        'category': category,
                      });
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text('Tracked: add_to_cart ($name)')),
                      );
                    },
                  ),
                ],
              ),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          Adopture.track('checkout_started', {
            'item_count': '3',
            'total': '249.48',
          });
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Tracked: checkout_started')),
          );
        },
        icon: const Icon(Icons.payment),
        label: const Text('Checkout'),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Revenue Demo Screen — demonstrates SDK revenue tracking methods
// ---------------------------------------------------------------------------

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

  @override
  State<RevenueDemoScreen> createState() => _RevenueDemoScreenState();
}

class _RevenueDemoScreenState extends State<RevenueDemoScreen> {
  @override
  void initState() {
    super.initState();
    Adopture.screen('RevenueDemoScreen');
  }

  void _show(String message) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message)),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Revenue Tracking')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          const Text(
            'Test revenue tracking methods',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          _ActionButton(
            label: 'Track Purchase',
            icon: Icons.shopping_cart_checkout,
            onPressed: () {
              Adopture.trackPurchase(
                productId: 'com.example.premium_monthly',
                price: 9.99,
                currency: 'USD',
                transactionId: 'txn_${DateTime.now().millisecondsSinceEpoch}',
              );
              _show('Tracked: purchase \$9.99 USD');
            },
          ),
          const SizedBox(height: 8),
          _ActionButton(
            label: 'Track One-Time Purchase',
            icon: Icons.sell,
            onPressed: () {
              Adopture.trackOneTimePurchase(
                productId: 'com.example.lifetime_access',
                price: 49.99,
                currency: 'USD',
                transactionId: 'txn_${DateTime.now().millisecondsSinceEpoch}',
              );
              _show('Tracked: one-time purchase \$49.99 USD');
            },
          ),
          const SizedBox(height: 8),
          _ActionButton(
            label: 'Track Renewal',
            icon: Icons.autorenew,
            onPressed: () {
              Adopture.trackRenewal(
                productId: 'com.example.premium_monthly',
                price: 9.99,
                currency: 'USD',
                transactionId: 'txn_${DateTime.now().millisecondsSinceEpoch}',
              );
              _show('Tracked: renewal \$9.99 USD');
            },
          ),
          const SizedBox(height: 8),
          _ActionButton(
            label: 'Track Trial Started',
            icon: Icons.free_breakfast,
            onPressed: () {
              Adopture.trackTrialStarted(
                productId: 'com.example.premium_monthly',
                expirationAt: DateTime.now()
                    .add(const Duration(days: 7))
                    .toUtc()
                    .toIso8601String(),
              );
              _show('Tracked: trial started (7 days)');
            },
          ),
          const SizedBox(height: 8),
          _ActionButton(
            label: 'Track Trial Converted',
            icon: Icons.check_circle,
            color: Colors.green,
            onPressed: () {
              Adopture.trackTrialConverted(
                productId: 'com.example.premium_monthly',
                price: 9.99,
                currency: 'USD',
                transactionId: 'txn_${DateTime.now().millisecondsSinceEpoch}',
              );
              _show('Tracked: trial converted \$9.99 USD');
            },
          ),
          const SizedBox(height: 8),
          _ActionButton(
            label: 'Track Cancellation',
            icon: Icons.cancel,
            color: Colors.orange,
            onPressed: () {
              Adopture.trackCancellation(
                productId: 'com.example.premium_monthly',
              );
              _show('Tracked: cancellation');
            },
          ),
          const SizedBox(height: 8),
          _ActionButton(
            label: 'Track Refund',
            icon: Icons.money_off,
            color: Colors.red,
            onPressed: () {
              Adopture.trackRefund(
                productId: 'com.example.premium_monthly',
                price: 9.99,
                currency: 'USD',
                transactionId: 'txn_${DateTime.now().millisecondsSinceEpoch}',
              );
              _show('Tracked: refund \$9.99 USD');
            },
          ),
          const SizedBox(height: 8),
          _ActionButton(
            label: 'Track Custom Revenue',
            icon: Icons.code,
            onPressed: () {
              Adopture.trackRevenue(const RevenueData(
                eventType: RevenueEventType.nonRenewingPurchase,
                productId: 'com.example.coin_pack_500',
                price: 4.99,
                currency: 'EUR',
                quantity: 2,
                store: Store.appStore,
              ));
              _show('Tracked: custom revenue 2x €4.99 EUR');
            },
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Stress Test Screen — batch events, offline simulation, etc.
// ---------------------------------------------------------------------------

class StressTestScreen extends StatefulWidget {
  final void Function(String message)? onLog;

  const StressTestScreen({super.key, this.onLog});

  @override
  State<StressTestScreen> createState() => _StressTestScreenState();
}

class _StressTestScreenState extends State<StressTestScreen> {
  bool _isSending = false;

  @override
  void initState() {
    super.initState();
    Adopture.screen('StressTestScreen');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Stress Test')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _SdkStateCard(),

          const SizedBox(height: 16),

          // Burst 10 events
          _TestCard(
            title: 'Burst: 10 Events',
            description: 'Send 10 track events rapidly to test batching.',
            icon: Icons.bolt,
            onRun: () => _burst(10),
          ),

          // Burst 50 events
          _TestCard(
            title: 'Burst: 50 Events',
            description: 'Send 50 events to test queue + flush threshold.',
            icon: Icons.flash_on,
            onRun: () => _burst(50),
          ),

          // Burst 200 events
          _TestCard(
            title: 'Burst: 200 Events',
            description:
                'Send 200 events to test multi-batch sending (max 100/request).',
            icon: Icons.rocket_launch,
            onRun: () => _burst(200),
          ),

          // Flush manually
          _TestCard(
            title: 'Manual Flush',
            description: 'Force flush all queued events to the server now.',
            icon: Icons.send,
            onRun: () async {
              setState(() => _isSending = true);
              await Adopture.flush();
              setState(() => _isSending = false);
              _showResult('Flushed. Queue: ${Adopture.queueLength}');
            },
          ),

          // Disable → track → enable → flush
          _TestCard(
            title: 'Opt-out / Opt-in Cycle',
            description:
                'Disable tracking, try to track events (should be dropped), '
                're-enable, track again, verify only post-enable events are queued.',
            icon: Icons.privacy_tip,
            onRun: _testOptOutCycle,
          ),

          // Reset test
          _TestCard(
            title: 'Reset SDK State',
            description:
                'Clears queue, user ID, starts new session. Verify session ID changes.',
            icon: Icons.restart_alt,
            onRun: () async {
              final oldSession = Adopture.sessionId;
              await Adopture.reset();
              final newSession = Adopture.sessionId;
              _showResult(
                'Session rotated\n'
                'Old: ${oldSession?.substring(0, 8)}...\n'
                'New: ${newSession?.substring(0, 8)}...\n'
                'Queue: ${Adopture.queueLength}',
              );
            },
          ),

          // Properties size test
          _TestCard(
            title: 'Large Properties',
            description:
                'Track event with many properties (near 500-char limit per value).',
            icon: Icons.data_object,
            onRun: () {
              final props = <String, String>{};
              for (var i = 0; i < 10; i++) {
                props['key_$i'] = 'v' * 100; // 100 chars each
              }
              Adopture.track('large_props_event', props);
              _showResult(
                'Tracked large_props_event with ${props.length} properties.\n'
                'Queue: ${Adopture.queueLength}',
              );
            },
          ),

          // Rapid screen switches
          _TestCard(
            title: 'Rapid Screen Views',
            description: 'Fire 20 screen() calls to simulate fast navigation.',
            icon: Icons.swap_horiz,
            onRun: () {
              final screens = [
                'Home',
                'Profile',
                'Settings',
                'Shop',
                'Cart',
                'Checkout',
                'OrderConfirm',
                'Search',
                'Filters',
                'Detail',
                'Reviews',
                'Wishlist',
                'Notifications',
                'Messages',
                'Help',
                'About',
                'Terms',
                'Privacy',
                'Account',
                'Billing',
              ];
              for (final s in screens) {
                Adopture.screen(s);
              }
              _showResult(
                'Fired 20 screen views.\nQueue: ${Adopture.queueLength}',
              );
            },
          ),

          if (_isSending)
            const Padding(
              padding: EdgeInsets.all(16),
              child: Center(child: CircularProgressIndicator()),
            ),

          const SizedBox(height: 80),
        ],
      ),
    );
  }

  Future<void> _burst(int count) async {
    for (var i = 0; i < count; i++) {
      Adopture.track('stress_event', {
        'index': '$i',
        'total': '$count',
        'batch': '${DateTime.now().millisecondsSinceEpoch}',
      });
    }
    setState(() {});
    _showResult('Sent $count events. Queue: ${Adopture.queueLength}');
    widget.onLog?.call('burst: $count events');
  }

  Future<void> _testOptOutCycle() async {
    // 1. Disable
    await Adopture.disable();
    final queueAfterDisable = Adopture.queueLength;

    // 2. Try to track (should be silently dropped)
    Adopture.track('should_be_dropped', {'phase': 'disabled'});
    final queueAfterDropped = Adopture.queueLength;

    // 3. Re-enable
    Adopture.enable();

    // 4. Track for real
    Adopture.track('after_reenable', {'phase': 'enabled'});
    final queueAfterEnable = Adopture.queueLength;

    _showResult(
      'Opt-out cycle complete:\n'
      '  After disable: queue=$queueAfterDisable\n'
      '  After dropped track: queue=$queueAfterDropped (should be same)\n'
      '  After re-enable track: queue=$queueAfterEnable (should be +1)',
    );
  }

  void _showResult(String message) {
    if (!mounted) return;
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Result'),
        content: Text(message, style: const TextStyle(fontFamily: 'monospace')),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Shared Widgets
// ---------------------------------------------------------------------------

class _SdkStateCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final ctx = Adopture.deviceContext;
    return Card(
      margin: const EdgeInsets.all(12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(
                  Adopture.isEnabled ? Icons.circle : Icons.circle_outlined,
                  size: 12,
                  color: Adopture.isEnabled ? Colors.green : Colors.red,
                ),
                const SizedBox(width: 8),
                Text(
                  Adopture.isEnabled ? 'TRACKING ON' : 'TRACKING OFF',
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 13,
                  ),
                ),
                const Spacer(),
                Text(
                  'Queue: ${Adopture.queueLength}',
                  style: const TextStyle(
                    fontFamily: 'monospace',
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            _InfoRow('Session', _truncate(Adopture.sessionId, 20)),
            _InfoRow('Endpoint', Adopture.endpoint),
            if (ctx != null) ...[
              _InfoRow(
                  'Device', '${ctx.os} ${ctx.osVersion} · ${ctx.deviceType}'),
              _InfoRow('App', 'v${ctx.appVersion} · ${ctx.locale}'),
              _InfoRow('Screen', '${ctx.screenWidth}x${ctx.screenHeight}'),
            ],
          ],
        ),
      ),
    );
  }
}

class _InfoRow extends StatelessWidget {
  final String label;
  final String value;

  const _InfoRow(this.label, this.value);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        children: [
          SizedBox(
            width: 70,
            child: Text(
              label,
              style: TextStyle(
                fontSize: 11,
                color: Colors.grey[500],
              ),
            ),
          ),
          Expanded(
            child: Text(
              value,
              style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
              overflow: TextOverflow.ellipsis,
            ),
          ),
        ],
      ),
    );
  }
}

class _NavChip extends StatelessWidget {
  final String label;
  final IconData icon;
  final VoidCallback onTap;

  const _NavChip({
    required this.label,
    required this.icon,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return ActionChip(
      avatar: Icon(icon, size: 18),
      label: Text(label),
      onPressed: onTap,
    );
  }
}

class _ActionButton extends StatelessWidget {
  final String label;
  final IconData icon;
  final VoidCallback onPressed;
  final Color? color;

  const _ActionButton({
    required this.label,
    required this.icon,
    required this.onPressed,
    this.color,
  });

  @override
  Widget build(BuildContext context) {
    return FilledButton.icon(
      onPressed: onPressed,
      icon: Icon(icon, size: 16),
      label: Text(label, style: const TextStyle(fontSize: 12)),
      style:
          color != null ? FilledButton.styleFrom(backgroundColor: color) : null,
    );
  }
}

class _TestCard extends StatelessWidget {
  final String title;
  final String description;
  final IconData icon;
  final VoidCallback onRun;

  const _TestCard({
    required this.title,
    required this.description,
    required this.icon,
    required this.onRun,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(bottom: 8),
      child: ListTile(
        leading: Icon(icon),
        title: Text(title, style: const TextStyle(fontWeight: FontWeight.w600)),
        subtitle: Text(description, style: const TextStyle(fontSize: 12)),
        trailing: FilledButton(
          onPressed: onRun,
          child: const Text('Run'),
        ),
      ),
    );
  }
}

class _SdkInfoTile extends StatelessWidget {
  const _SdkInfoTile();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            'SDK Debug Info',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          _InfoRow('Initialized', '${Adopture.isInitialized}'),
          _InfoRow('Enabled', '${Adopture.isEnabled}'),
          _InfoRow('Queue', '${Adopture.queueLength}'),
          _InfoRow('Session', _truncate(Adopture.sessionId, 20)),
          _InfoRow('Endpoint', Adopture.endpoint),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

String _truncate(String? s, int maxLen) {
  if (s == null) return '-';
  return s.length > maxLen ? '${s.substring(0, maxLen)}...' : s;
}

String _timestamp() {
  final now = DateTime.now();
  return '${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
}

String _pad(int n) => n.toString().padLeft(2, '0');
0
likes
0
points
158
downloads

Documentation

Documentation

Publisher

verified publisheradopture.com

Weekly Downloads

Privacy-first mobile analytics SDK for Flutter. Offline-first event tracking, automatic sessions, revenue analytics, and privacy-preserving hashed IDs.

Homepage
Repository (GitHub)
View/report issues

Topics

#analytics #privacy #mobile #tracking #events

License

unknown (license)

Dependencies

connectivity_plus, crypto, device_info_plus, flutter, http, package_info_plus, path_provider, shared_preferences, sqflite, uuid

More

Packages that depend on adopture