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

Flutter client SDK for Sankofa Analytics with offline queueing and session replay.

example/lib/main.dart

import 'dart:math';

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return SankofaReplayBoundary(
      child: MaterialApp(
        title: 'Sankofa Demo',
        navigatorObservers: [SankofaNavigatorObserver()],
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(
            seedColor: const Color(0xFF6C5CE7),
            brightness: Brightness.dark,
          ),
          useMaterial3: true,
          scaffoldBackgroundColor: const Color(0xFF0F0F1A),
          cardTheme: CardThemeData(
            color: const Color(0xFF1A1A2E),
            elevation: 0,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(16),
              side: BorderSide(color: Colors.white.withValues(alpha: 0.06)),
            ),
          ),
          inputDecorationTheme: InputDecorationTheme(
            filled: true,
            fillColor: const Color(0xFF16162A),
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(12),
              borderSide: BorderSide(
                color: Colors.white.withValues(alpha: 0.1),
              ),
            ),
            enabledBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(12),
              borderSide: BorderSide(
                color: Colors.white.withValues(alpha: 0.1),
              ),
            ),
            focusedBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(12),
              borderSide: const BorderSide(
                color: Color(0xFF6C5CE7),
                width: 1.5,
              ),
            ),
            contentPadding: const EdgeInsets.symmetric(
              horizontal: 16,
              vertical: 14,
            ),
            hintStyle: TextStyle(
              color: Colors.white.withValues(alpha: 0.3),
              fontSize: 14,
            ),
            labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.6)),
          ),
        ),
        home: const SetupScreen(),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// SETUP SCREEN — Configure Engine URL & API Key before initializing SDK
// ─────────────────────────────────────────────────────────────────────────────

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

  @override
  State<SetupScreen> createState() => _SetupScreenState();
}

class _SetupScreenState extends State<SetupScreen>
    with SingleTickerProviderStateMixin {
  static const _engineUrlFieldKey = Key('setup-engine-url-field');
  static const _apiKeyFieldKey = Key('setup-api-key-field');
  static const _connectButtonKey = Key('setup-connect-button');

  String _getDefaultEngineUrl() {
    // if (kIsWeb) return 'http://localhost:8080';
    // if (Platform.isAndroid) return 'http://10.0.2.2:8080';
    // return 'http://127.0.0.1:8080';
    return 'https://api.sankofa.dev';
  }

  late final _engineUrlController = TextEditingController(
    text: _getDefaultEngineUrl(),
  );
  final _apiKeyController = TextEditingController();

  bool _connecting = false;
  bool _debugMode = true;
  bool _trackLifecycleEvents = true;
  bool _enableSessionReplay = true;
  SankofaReplayMode _replayMode = SankofaReplayMode.wireframe;

  late AnimationController _pulseController;
  late Animation<double> _pulseAnimation;

  @override
  void initState() {
    super.initState();
    _pulseController = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )..repeat(reverse: true);
    _pulseAnimation = Tween<double>(begin: 0.6, end: 1.0).animate(
      CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
    );
  }

  @override
  void dispose() {
    _pulseController.dispose();
    _engineUrlController.dispose();
    _apiKeyController.dispose();
    super.dispose();
  }

  Future<void> _connect() async {
    final url = _engineUrlController.text.trim();
    final key = _apiKeyController.text.trim();

    if (url.isEmpty || key.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: const Text('Please fill in both fields'),
          backgroundColor: Colors.red.shade700,
          behavior: SnackBarBehavior.floating,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(10),
          ),
        ),
      );
      return;
    }

    setState(() => _connecting = true);

    try {
      await Sankofa.instance.init(
        apiKey: key,
        endpoint: url,
        debug: _debugMode,
        trackLifecycleEvents: _trackLifecycleEvents,
        enableSessionReplay: _enableSessionReplay,
        replayMode: _replayMode,
      );

      if (!mounted) return;

      Navigator.of(context).pushReplacement(
        PageRouteBuilder(
          transitionDuration: const Duration(milliseconds: 600),
          pageBuilder: (context, animation, _) => const EventTesterScreen(),
          transitionsBuilder: (context, animation, _, child) {
            return FadeTransition(
              opacity: animation,
              child: SlideTransition(
                position:
                    Tween<Offset>(
                      begin: const Offset(0, 0.05),
                      end: Offset.zero,
                    ).animate(
                      CurvedAnimation(parent: animation, curve: Curves.easeOut),
                    ),
                child: child,
              ),
            );
          },
        ),
      );
    } catch (e) {
      if (!mounted) return;
      setState(() => _connecting = false);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Connection failed: $e'),
          backgroundColor: Colors.red.shade700,
          behavior: SnackBarBehavior.floating,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(10),
          ),
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SingleChildScrollView(
          padding: const EdgeInsets.symmetric(horizontal: 28),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // Logo / Title
              AnimatedBuilder(
                animation: _pulseAnimation,
                builder: (context, child) {
                  return Opacity(opacity: _pulseAnimation.value, child: child);
                },
                child: Container(
                  width: 80,
                  height: 80,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    gradient: const LinearGradient(
                      colors: [Color(0xFF6C5CE7), Color(0xFFA29BFE)],
                      begin: Alignment.topLeft,
                      end: Alignment.bottomRight,
                    ),
                    boxShadow: [
                      BoxShadow(
                        color: const Color(0xFF6C5CE7).withValues(alpha: 0.4),
                        blurRadius: 24,
                        spreadRadius: 2,
                      ),
                    ],
                  ),
                  child: const Icon(
                    Icons.analytics_rounded,
                    color: Colors.white,
                    size: 40,
                  ),
                ),
              ),
              const SizedBox(height: 24),
              const Text(
                'Sankofa',
                style: TextStyle(
                  fontSize: 32,
                  fontWeight: FontWeight.w800,
                  letterSpacing: -0.5,
                  color: Colors.white,
                ),
              ),
              const SizedBox(height: 6),
              Text(
                'Event Tester',
                style: TextStyle(
                  fontSize: 15,
                  fontWeight: FontWeight.w500,
                  color: Colors.white.withValues(alpha: 0.4),
                  letterSpacing: 2,
                ),
              ),
              const SizedBox(height: 48),

              // Config Card
              Container(
                padding: const EdgeInsets.all(24),
                decoration: BoxDecoration(
                  color: const Color(0xFF1A1A2E),
                  borderRadius: BorderRadius.circular(20),
                  border: Border.all(
                    color: Colors.white.withValues(alpha: 0.06),
                  ),
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Icon(
                          Icons.settings_rounded,
                          color: Colors.white.withValues(alpha: 0.5),
                          size: 18,
                        ),
                        const SizedBox(width: 8),
                        Text(
                          'CONNECTION SETTINGS',
                          style: TextStyle(
                            fontSize: 12,
                            fontWeight: FontWeight.w700,
                            letterSpacing: 1.5,
                            color: Colors.white.withValues(alpha: 0.5),
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 20),

                    // Engine URL
                    const _FieldLabel(label: 'Engine URL'),
                    const SizedBox(height: 8),
                    TextField(
                      key: _engineUrlFieldKey,
                      controller: _engineUrlController,
                      style: const TextStyle(color: Colors.white, fontSize: 14),
                      decoration: const InputDecoration(
                        hintText: 'http://localhost:8080',
                        prefixIcon: Icon(
                          Icons.dns_rounded,
                          size: 20,
                          color: Color(0xFF6C5CE7),
                        ),
                      ),
                    ),
                    const SizedBox(height: 18),

                    // API Key
                    const _FieldLabel(label: 'API Key'),
                    const SizedBox(height: 8),
                    SankofaMask(
                      child: TextField(
                        key: _apiKeyFieldKey,
                        controller: _apiKeyController,
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 14,
                        ),
                        obscureText: true,
                        decoration: const InputDecoration(
                          hintText: 'sk_test_...',
                          prefixIcon: Icon(
                            Icons.key_rounded,
                            size: 20,
                            color: Color(0xFF6C5CE7),
                          ),
                        ),
                      ),
                    ),
                    const SizedBox(height: 18),

                    // Debug toggle
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Row(
                          children: [
                            Icon(
                              Icons.bug_report_rounded,
                              size: 18,
                              color: Colors.white.withValues(alpha: 0.5),
                            ),
                            const SizedBox(width: 8),
                            Text(
                              'Debug Mode',
                              style: TextStyle(
                                color: Colors.white.withValues(alpha: 0.7),
                                fontSize: 14,
                              ),
                            ),
                          ],
                        ),
                        Switch(
                          value: _debugMode,
                          onChanged: (v) => setState(() => _debugMode = v),
                          activeThumbColor: const Color(0xFF6C5CE7),
                        ),
                      ],
                    ),
                    const SizedBox(height: 12),
                    // Track Lifecycle toggle
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Row(
                          children: [
                            Icon(
                              Icons.history_toggle_off_rounded,
                              size: 18,
                              color: Colors.white.withValues(alpha: 0.5),
                            ),
                            const SizedBox(width: 8),
                            Text(
                              'Track Lifecycle',
                              style: TextStyle(
                                color: Colors.white.withValues(alpha: 0.7),
                                fontSize: 14,
                              ),
                            ),
                          ],
                        ),
                        Switch(
                          value: _trackLifecycleEvents,
                          onChanged: (v) =>
                              setState(() => _trackLifecycleEvents = v),
                          activeThumbColor: const Color(0xFF6C5CE7),
                        ),
                      ],
                    ),
                    const SizedBox(height: 12),
                    // Session Replay toggle
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Row(
                          children: [
                            Icon(
                              Icons.videocam_rounded,
                              size: 18,
                              color: Colors.white.withValues(alpha: 0.5),
                            ),
                            const SizedBox(width: 8),
                            Text(
                              'Session Replay',
                              style: TextStyle(
                                color: Colors.white.withValues(alpha: 0.7),
                                fontSize: 14,
                              ),
                            ),
                          ],
                        ),
                        Switch(
                          value: _enableSessionReplay,
                          onChanged: (v) =>
                              setState(() => _enableSessionReplay = v),
                          activeThumbColor: const Color(0xFF6C5CE7),
                        ),
                      ],
                    ),
                    if (_enableSessionReplay) ...[
                      const SizedBox(height: 12),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          Row(
                            children: [
                              Icon(
                                Icons.switch_video_rounded,
                                size: 18,
                                color: Colors.white.withValues(alpha: 0.5),
                              ),
                              const SizedBox(width: 8),
                              Text(
                                'Replay Mode',
                                style: TextStyle(
                                  color: Colors.white.withValues(alpha: 0.7),
                                  fontSize: 14,
                                ),
                              ),
                            ],
                          ),
                          DropdownButton<SankofaReplayMode>(
                            value: _replayMode,
                            dropdownColor: const Color(0xFF16162A),
                            underline: const SizedBox(),
                            icon: const Icon(
                              Icons.arrow_drop_down,
                              color: Color(0xFF6C5CE7),
                            ),
                            style: const TextStyle(
                              color: Colors.white,
                              fontSize: 14,
                            ),
                            onChanged: (SankofaReplayMode? newValue) {
                              if (newValue != null) {
                                setState(() {
                                  _replayMode = newValue;
                                });
                              }
                            },
                            items: SankofaReplayMode.values.map((
                              SankofaReplayMode mode,
                            ) {
                              return DropdownMenuItem<SankofaReplayMode>(
                                value: mode,
                                child: Text(mode.name),
                              );
                            }).toList(),
                          ),
                        ],
                      ),
                    ],
                  ],
                ),
              ),

              const SizedBox(height: 28),

              // Connect Button
              SizedBox(
                width: double.infinity,
                height: 52,
                child: FilledButton(
                  key: _connectButtonKey,
                  onPressed: _connecting ? null : _connect,
                  style: FilledButton.styleFrom(
                    backgroundColor: const Color(0xFF6C5CE7),
                    disabledBackgroundColor: const Color(
                      0xFF6C5CE7,
                    ).withValues(alpha: 0.5),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(14),
                    ),
                  ),
                  child: _connecting
                      ? const SizedBox(
                          width: 22,
                          height: 22,
                          child: CircularProgressIndicator(
                            strokeWidth: 2.5,
                            color: Colors.white,
                          ),
                        )
                      : const Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Icon(Icons.bolt_rounded, size: 20),
                            SizedBox(width: 8),
                            Text(
                              'Initialize & Connect',
                              style: TextStyle(
                                fontSize: 16,
                                fontWeight: FontWeight.w600,
                              ),
                            ),
                          ],
                        ),
                ),
              ),

              const SizedBox(height: 16),
              Text(
                'Tip: Use 10.0.2.2 for Android emulator\nor localhost for iOS simulator',
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontSize: 12,
                  color: Colors.white.withValues(alpha: 0.3),
                  height: 1.5,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _FieldLabel extends StatelessWidget {
  final String label;
  const _FieldLabel({required this.label});

  @override
  Widget build(BuildContext context) {
    return Text(
      label,
      style: TextStyle(
        fontSize: 13,
        fontWeight: FontWeight.w600,
        color: Colors.white.withValues(alpha: 0.6),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// EVENT TESTER SCREEN — Main event testing dashboard
// ─────────────────────────────────────────────────────────────────────────────

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

  @override
  State<EventTesterScreen> createState() => _EventTesterScreenState();
}

class _EventTesterScreenState extends State<EventTesterScreen> {
  int _counter = 0;
  bool _autoSending = false;

  // Custom event fields
  final _eventNameController = TextEditingController();
  final List<MapEntry<TextEditingController, TextEditingController>>
  _customProps = [];
  bool _sendingCustom = false;

  @override
  void dispose() {
    _eventNameController.dispose();
    for (final entry in _customProps) {
      entry.key.dispose();
      entry.value.dispose();
    }
    super.dispose();
  }

  void _addProperty() {
    setState(() {
      _customProps.add(
        MapEntry(TextEditingController(), TextEditingController()),
      );
    });
  }

  void _removeProperty(int index) {
    setState(() {
      _customProps[index].key.dispose();
      _customProps[index].value.dispose();
      _customProps.removeAt(index);
    });
  }

  Future<void> _sendCustomEvent() async {
    final eventName = _eventNameController.text.trim();
    if (eventName.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: const Text('Please enter an event name'),
          backgroundColor: Colors.red.shade700,
          behavior: SnackBarBehavior.floating,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(10),
          ),
        ),
      );
      return;
    }

    setState(() => _sendingCustom = true);

    final props = <String, dynamic>{};
    for (final entry in _customProps) {
      final key = entry.key.text.trim();
      final value = entry.value.text.trim();
      if (key.isNotEmpty) {
        // Try to parse as number or bool, otherwise keep as string
        if (double.tryParse(value) != null) {
          props[key] = double.parse(value);
        } else if (value.toLowerCase() == 'true' ||
            value.toLowerCase() == 'false') {
          props[key] = value.toLowerCase() == 'true';
        } else {
          props[key] = value;
        }
      }
    }

    try {
      await Sankofa.instance.track(eventName, props.isNotEmpty ? props : null);

      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Row(
            children: [
              const Icon(Icons.check_circle, color: Colors.white, size: 18),
              const SizedBox(width: 8),
              Expanded(
                child: Text(
                  'Sent "$eventName"${props.isNotEmpty ? ' with ${props.length} prop(s)' : ''}',
                ),
              ),
            ],
          ),
          backgroundColor: const Color(0xFF00B894),
          behavior: SnackBarBehavior.floating,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(10),
          ),
        ),
      );
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Failed: $e'),
          backgroundColor: Colors.red.shade700,
          behavior: SnackBarBehavior.floating,
        ),
      );
    }

    setState(() => _sendingCustom = false);
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
    Sankofa.instance.track('button_click', {
      'button_id': 'increment_fab',
      'current_count': _counter,
    });
  }

  Future<void> _simulateUserJourney() async {
    await Sankofa.instance.track('page_view', {'path': '/home'});
    await Future.delayed(const Duration(milliseconds: 500));
    await Sankofa.instance.track('view_item', {
      'item_id': 'prod_123',
      'price': 29.99,
    });
    await Future.delayed(const Duration(milliseconds: 800));
    await Sankofa.instance.track('add_to_cart', {
      'item_id': 'prod_123',
      'quantity': 1,
    });
    await Future.delayed(const Duration(milliseconds: 1200));
    await Sankofa.instance.track('begin_checkout');

    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: const Row(
          children: [
            Icon(Icons.shopping_cart, color: Colors.white, size: 18),
            SizedBox(width: 8),
            Text('Simulated Journey Sent!'),
          ],
        ),
        backgroundColor: const Color(0xFF6C5CE7),
        behavior: SnackBarBehavior.floating,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
      ),
    );
  }

  void _toggleAutoSpam() async {
    setState(() {
      _autoSending = !_autoSending;
    });

    if (_autoSending) {
      while (_autoSending) {
        final r = Random();
        await Sankofa.instance.track('auto_event', {
          'random_val': r.nextInt(100),
          'spam_mode': true,
        });
        await Future.delayed(
          const Duration(milliseconds: 100),
        ); // 10 events/sec
        if (!mounted) break;
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: const Color(0xFF16162A),
        surfaceTintColor: Colors.transparent,
        title: const Row(
          children: [
            Icon(Icons.analytics_rounded, color: Color(0xFF6C5CE7), size: 22),
            SizedBox(width: 10),
            Text(
              'Sankofa Tester',
              style: TextStyle(fontWeight: FontWeight.w700, fontSize: 18),
            ),
          ],
        ),
        actions: [
          IconButton(
            onPressed: () {
              // Navigate back to setup
              Sankofa.instance.reset();
              Navigator.of(context).pushReplacement(
                MaterialPageRoute(builder: (_) => const SetupScreen()),
              );
            },
            icon: Icon(
              Icons.settings_rounded,
              color: Colors.white.withValues(alpha: 0.5),
            ),
            tooltip: 'Configuration',
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // ── Custom Event Section ──────────────────────────────
            _buildSectionHeader(
              icon: Icons.send_rounded,
              title: 'Custom Event',
              color: const Color(0xFF00B894),
            ),
            const SizedBox(height: 14),
            Container(
              padding: const EdgeInsets.all(20),
              decoration: BoxDecoration(
                color: const Color(0xFF1A1A2E),
                borderRadius: BorderRadius.circular(16),
                border: Border.all(color: Colors.white.withValues(alpha: 0.06)),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // Event Name
                  TextField(
                    controller: _eventNameController,
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 15,
                      fontWeight: FontWeight.w500,
                    ),
                    decoration: InputDecoration(
                      hintText: 'Event name (e.g. purchase_completed)',
                      prefixIcon: const Icon(
                        Icons.label_rounded,
                        size: 20,
                        color: Color(0xFF00B894),
                      ),
                      suffixIcon: _eventNameController.text.isNotEmpty
                          ? IconButton(
                              icon: Icon(
                                Icons.clear,
                                size: 18,
                                color: Colors.white.withValues(alpha: 0.3),
                              ),
                              onPressed: () {
                                _eventNameController.clear();
                                setState(() {});
                              },
                            )
                          : null,
                    ),
                    onChanged: (_) => setState(() {}),
                  ),
                  const SizedBox(height: 16),

                  // Properties Header
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(
                        'Properties',
                        style: TextStyle(
                          fontSize: 13,
                          fontWeight: FontWeight.w600,
                          color: Colors.white.withValues(alpha: 0.6),
                        ),
                      ),
                      TextButton.icon(
                        onPressed: _addProperty,
                        icon: const Icon(Icons.add, size: 16),
                        label: const Text('Add'),
                        style: TextButton.styleFrom(
                          foregroundColor: const Color(0xFF00B894),
                          textStyle: const TextStyle(fontSize: 13),
                          padding: const EdgeInsets.symmetric(horizontal: 12),
                          visualDensity: VisualDensity.compact,
                        ),
                      ),
                    ],
                  ),

                  // Properties List
                  if (_customProps.isEmpty)
                    Padding(
                      padding: const EdgeInsets.symmetric(vertical: 8),
                      child: Text(
                        'No properties yet. Tap "Add" to include key-value pairs.',
                        style: TextStyle(
                          fontSize: 13,
                          color: Colors.white.withValues(alpha: 0.25),
                          fontStyle: FontStyle.italic,
                        ),
                      ),
                    )
                  else
                    ..._customProps.asMap().entries.map((entry) {
                      final index = entry.key;
                      final kv = entry.value;
                      return Padding(
                        padding: const EdgeInsets.only(bottom: 10),
                        child: Row(
                          children: [
                            Expanded(
                              flex: 2,
                              child: TextField(
                                controller: kv.key,
                                style: const TextStyle(
                                  color: Colors.white,
                                  fontSize: 13,
                                ),
                                decoration: InputDecoration(
                                  hintText: 'Key',
                                  contentPadding: const EdgeInsets.symmetric(
                                    horizontal: 12,
                                    vertical: 10,
                                  ),
                                  hintStyle: TextStyle(
                                    color: Colors.white.withValues(alpha: 0.2),
                                    fontSize: 13,
                                  ),
                                ),
                              ),
                            ),
                            const SizedBox(width: 8),
                            Expanded(
                              flex: 3,
                              child: TextField(
                                controller: kv.value,
                                style: const TextStyle(
                                  color: Colors.white,
                                  fontSize: 13,
                                ),
                                decoration: InputDecoration(
                                  hintText: 'Value',
                                  contentPadding: const EdgeInsets.symmetric(
                                    horizontal: 12,
                                    vertical: 10,
                                  ),
                                  hintStyle: TextStyle(
                                    color: Colors.white.withValues(alpha: 0.2),
                                    fontSize: 13,
                                  ),
                                ),
                              ),
                            ),
                            const SizedBox(width: 4),
                            InkWell(
                              onTap: () => _removeProperty(index),
                              borderRadius: BorderRadius.circular(8),
                              child: Padding(
                                padding: const EdgeInsets.all(6),
                                child: Icon(
                                  Icons.remove_circle_outline,
                                  size: 20,
                                  color: Colors.red.withValues(alpha: 0.7),
                                ),
                              ),
                            ),
                          ],
                        ),
                      );
                    }),

                  const SizedBox(height: 12),

                  // Send Button
                  SizedBox(
                    width: double.infinity,
                    height: 46,
                    child: FilledButton.icon(
                      onPressed: _sendingCustom ? null : _sendCustomEvent,
                      icon: _sendingCustom
                          ? const SizedBox(
                              width: 16,
                              height: 16,
                              child: CircularProgressIndicator(
                                strokeWidth: 2,
                                color: Colors.white,
                              ),
                            )
                          : const Icon(Icons.send_rounded, size: 18),
                      label: Text(
                        _sendingCustom ? 'Sending...' : 'Send Event',
                        style: const TextStyle(fontWeight: FontWeight.w600),
                      ),
                      style: FilledButton.styleFrom(
                        backgroundColor: const Color(0xFF00B894),
                        disabledBackgroundColor: const Color(
                          0xFF00B894,
                        ).withValues(alpha: 0.5),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),

            const SizedBox(height: 32),

            // ── Quick Actions ────────────────────────────────────
            _buildSectionHeader(
              icon: Icons.bolt_rounded,
              title: 'Quick Actions',
              color: const Color(0xFF6C5CE7),
            ),
            const SizedBox(height: 14),
            Wrap(
              spacing: 10,
              runSpacing: 10,
              children: [
                _QuickActionChip(
                  label: 'Identify User',
                  icon: Icons.person_rounded,
                  color: const Color(0xFF6C5CE7),
                  onTap: () => Sankofa.instance.identify(
                    'user_${Random().nextInt(1000)}',
                  ),
                ),
                _QuickActionChip(
                  label: 'Set Profile',
                  icon: Icons.badge_rounded,
                  color: const Color(0xFF0984E3),
                  onTap: () => Sankofa.instance.peopleSet({
                    'plan': 'premium',
                    'email': 'user@example.com',
                    'ltv': Random().nextInt(500),
                  }),
                ),
                _QuickActionChip(
                  label: 'Reset Identity',
                  icon: Icons.logout_rounded,
                  color: const Color(0xFFE17055),
                  onTap: () => Sankofa.instance.reset(),
                ),
                _QuickActionChip(
                  label: 'Track Tap',
                  icon: Icons.touch_app_rounded,
                  color: const Color(0xFFFDAA5E),
                  onTap: () => Sankofa.instance.track('simple_tap'),
                ),
                _QuickActionChip(
                  label: 'Track Purchase Error',
                  icon: Icons.touch_app_rounded,
                  color: const Color.fromARGB(255, 207, 0, 134),
                  onTap: () => Sankofa.instance.track('purchase_error'),
                ),
                _QuickActionChip(
                  label: 'Purchase Flow',
                  icon: Icons.shopping_cart_rounded,
                  color: const Color(0xFF00CEC9),
                  onTap: _simulateUserJourney,
                ),
              ],
            ),

            const SizedBox(height: 32),

            // ── Stress Test ──────────────────────────────────────
            _buildSectionHeader(
              icon: Icons.speed_rounded,
              title: 'Stress Test',
              color: const Color(0xFFE74C3C),
            ),
            const SizedBox(height: 14),
            Container(
              width: double.infinity,
              padding: const EdgeInsets.all(20),
              decoration: BoxDecoration(
                color: const Color(0xFF1A1A2E),
                borderRadius: BorderRadius.circular(16),
                border: Border.all(
                  color: _autoSending
                      ? Colors.red.withValues(alpha: 0.3)
                      : Colors.white.withValues(alpha: 0.06),
                ),
              ),
              child: Column(
                children: [
                  Icon(
                    _autoSending
                        ? Icons.sensors_rounded
                        : Icons.sensors_off_rounded,
                    size: 36,
                    color: _autoSending
                        ? Colors.red
                        : Colors.white.withValues(alpha: 0.3),
                  ),
                  const SizedBox(height: 12),
                  Text(
                    _autoSending ? 'Sending 10 events/sec' : 'Auto-spam mode',
                    style: TextStyle(
                      color: _autoSending
                          ? Colors.red.shade300
                          : Colors.white.withValues(alpha: 0.5),
                      fontSize: 14,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                  const SizedBox(height: 16),
                  SizedBox(
                    width: double.infinity,
                    height: 46,
                    child: FilledButton.icon(
                      onPressed: _toggleAutoSpam,
                      icon: Icon(
                        _autoSending
                            ? Icons.stop_rounded
                            : Icons.play_arrow_rounded,
                        size: 20,
                      ),
                      label: Text(
                        _autoSending ? 'Stop' : 'Start Spam',
                        style: const TextStyle(fontWeight: FontWeight.w600),
                      ),
                      style: FilledButton.styleFrom(
                        backgroundColor: _autoSending
                            ? Colors.red.shade700
                            : const Color(0xFF2D3436),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),

            const SizedBox(height: 40),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Track button_click',
        backgroundColor: const Color(0xFF6C5CE7),
        child: const Icon(Icons.add, color: Colors.white),
      ),
    );
  }

  Widget _buildSectionHeader({
    required IconData icon,
    required String title,
    required Color color,
  }) {
    return Row(
      children: [
        Container(
          width: 32,
          height: 32,
          decoration: BoxDecoration(
            color: color.withValues(alpha: 0.15),
            borderRadius: BorderRadius.circular(8),
          ),
          child: Icon(icon, size: 18, color: color),
        ),
        const SizedBox(width: 10),
        Text(
          title,
          style: const TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.w700,
            color: Colors.white,
          ),
        ),
      ],
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Quick Action Chip Widget
// ─────────────────────────────────────────────────────────────────────────────

class _QuickActionChip extends StatelessWidget {
  final String label;
  final IconData icon;
  final Color color;
  final VoidCallback onTap;

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

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.transparent,
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        splashColor: color.withValues(alpha: 0.2),
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
          decoration: BoxDecoration(
            color: color.withValues(alpha: 0.1),
            borderRadius: BorderRadius.circular(12),
            border: Border.all(color: color.withValues(alpha: 0.2)),
          ),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              Icon(icon, size: 16, color: color),
              const SizedBox(width: 8),
              Text(
                label,
                style: TextStyle(
                  color: color,
                  fontSize: 13,
                  fontWeight: FontWeight.w600,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
1
likes
150
points
135
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter client SDK for Sankofa Analytics with offline queueing and session replay.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

app_links, carrier_info, connectivity_plus, device_info_plus, flutter, http, package_info_plus, shared_preferences, uuid

More

Packages that depend on sankofa_flutter