b2metric_sdk 0.2.0 copy "b2metric_sdk: ^0.2.0" to clipboard
b2metric_sdk: ^0.2.0 copied to clipboard

B2Metric analytics SDK for Flutter. Collects events, manages sessions, and supports push notifications on Android and iOS.

example/lib/main.dart

import 'dart:convert';

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

void main() => runApp(const TestbedApp());

// ─── Types ────────────────────────────────────────────────────────────────────

enum TerminalLevel { debug, info, warn, error, app }

class LogEntry {
  final String time;
  final TerminalLevel level;
  final String message;

  LogEntry(this.time, this.level, this.message);
}

class Preset {
  final String label;
  final String eventName;
  final String? properties;
  final String? itemProperties;

  const Preset({
    required this.label,
    required this.eventName,
    this.properties,
    this.itemProperties,
  });
}

// ─── Constants ────────────────────────────────────────────────────────────────

const _sdkLogLevels = LogLevel.values;

String _pretty(Object obj) =>
    const JsonEncoder.withIndent('  ').convert(obj);

final _presets = <Preset>[
  Preset(
    label: 'page_view',
    eventName: 'page_view',
    properties: _pretty({'screen': 'home', 'referrer': 'push'}),
  ),
  Preset(
    label: 'button_click',
    eventName: 'button_click',
    properties: _pretty({'button': 'checkout', 'section': 'cart'}),
  ),
  Preset(
    label: 'add_to_cart',
    eventName: 'add_to_cart',
    properties: _pretty({'total': 5700, 'currency': 'TRY'}),
    itemProperties: _pretty([
      {'id': 'SKU-001', 'name': 'Koşu Ayakkabısı', 'price': 2850, 'quantity': 1},
      {'id': 'SKU-002', 'name': 'Spor Çorap', 'price': 150, 'quantity': 3},
    ]),
  ),
  Preset(
    label: 'purchase',
    eventName: 'purchase',
    properties: _pretty({
      'order_id': 'ORD-4521',
      'total': 5700,
      'currency': 'TRY',
      'payment': 'credit_card',
    }),
    itemProperties: _pretty([
      {'id': 'SKU-001', 'name': 'Koşu Ayakkabısı', 'price': 2850, 'quantity': 1},
    ]),
  ),
  Preset(
    label: 'search',
    eventName: 'search',
    properties: _pretty({'query': 'koşu ayakkabısı', 'results': 42}),
  ),
  Preset(
    label: 'login',
    eventName: 'login',
    properties: _pretty({'method': 'email', 'success': true}),
  ),
];

const _levelColors = {
  TerminalLevel.debug: Color(0xFF6B7280),
  TerminalLevel.info: Color(0xFF38BDF8),
  TerminalLevel.warn: Color(0xFFFBBF24),
  TerminalLevel.error: Color(0xFFF87171),
  TerminalLevel.app: Color(0xFF4ADE80),
};

// ─── App root ─────────────────────────────────────────────────────────────────

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'B2Metric Testbed',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        brightness: Brightness.dark,
        scaffoldBackgroundColor: const Color(0xFF0F172A),
        fontFamily: 'monospace',
      ),
      home: const TestbedScreen(),
    );
  }
}

// ─── Screen ───────────────────────────────────────────────────────────────────

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

  @override
  State<TestbedScreen> createState() => _TestbedScreenState();
}

class _TestbedScreenState extends State<TestbedScreen> {
  final _apiKey = TextEditingController();
  final _appIdentifier = TextEditingController();
  final _batchSize = TextEditingController(text: '20');
  final _flushInterval = TextEditingController(text: '30');
  final _sessionTimeout = TextEditingController(text: '30');
  LogLevel _logLevel = LogLevel.debug;
  bool _sdkReady = false;

  final _eventName = TextEditingController();
  final _propsJson = TextEditingController();
  final _itemPropsJson = TextEditingController();

  final List<LogEntry> _logs = [];
  final ScrollController _terminalScroll = ScrollController();

  @override
  void dispose() {
    _apiKey.dispose();
    _appIdentifier.dispose();
    _batchSize.dispose();
    _flushInterval.dispose();
    _sessionTimeout.dispose();
    _eventName.dispose();
    _propsJson.dispose();
    _itemPropsJson.dispose();
    _terminalScroll.dispose();
    super.dispose();
  }

  void _addLog(TerminalLevel level, String message) {
    final ts = DateTime.now().toUtc().toIso8601String().substring(11, 23);
    setState(() {
      _logs.insert(0, LogEntry(ts, level, message));
    });
    if (_terminalScroll.hasClients) {
      _terminalScroll.jumpTo(0);
    }
  }

  TerminalLevel _mapLevel(LogLevel l) => switch (l) {
        LogLevel.debug => TerminalLevel.debug,
        LogLevel.info => TerminalLevel.info,
        LogLevel.warning => TerminalLevel.warn,
        LogLevel.error => TerminalLevel.error,
        LogLevel.off => TerminalLevel.debug,
      };

  // ─── Handlers ──────────────────────────────────────────────────────────────

  Future<void> _handleInit() async {
    final key = _apiKey.text.trim();
    if (key.isEmpty) {
      _addLog(TerminalLevel.warn, 'API key is required');
      return;
    }
    final appId = _appIdentifier.text.trim();
    if (appId.isEmpty) {
      _addLog(TerminalLevel.warn, 'App identifier is required');
      return;
    }
    try {
      await B2Metric.instance.init(B2MetricConfig(
        apiKey: key,
        appIdentifier: appId,
        batchSize: int.tryParse(_batchSize.text) ?? 20,
        flushIntervalSeconds: int.tryParse(_flushInterval.text) ?? 30,
        sessionTimeoutMinutes: int.tryParse(_sessionTimeout.text) ?? 30,
        logLevel: _logLevel,
        onLog: (level, message) {
          final clean = message.replaceFirst(RegExp(r'\[B2Metric\]\s?'), '');
          _addLog(_mapLevel(level), clean);
        },
      ));
      setState(() => _sdkReady = true);
      _addLog(TerminalLevel.app, 'SDK initialized successfully');
    } catch (e) {
      _addLog(TerminalLevel.error, 'Init failed: $e');
    }
  }

  Future<void> _handleDestroy() async {
    try {
      await B2Metric.instance.destroy();
      setState(() => _sdkReady = false);
      _addLog(TerminalLevel.app, 'SDK destroyed');
    } catch (e) {
      _addLog(TerminalLevel.error, 'Destroy failed: $e');
    }
  }

  Future<void> _handleFlush() async {
    try {
      await B2Metric.instance.flush();
      _addLog(TerminalLevel.app, 'Manual flush triggered');
    } catch (e) {
      _addLog(TerminalLevel.error, 'Flush failed: $e');
    }
  }

  void _fillPreset(Preset p) {
    _eventName.text = p.eventName;
    _propsJson.text = p.properties ?? '';
    _itemPropsJson.text = p.itemProperties ?? '';
  }

  void _handleSendEvent() {
    final name = _eventName.text.trim();
    if (name.isEmpty) {
      _addLog(TerminalLevel.warn, 'Event name is required');
      return;
    }
    try {
      Map<String, dynamic>? properties;
      List<Map<String, dynamic>>? itemProperties;

      if (_propsJson.text.trim().isNotEmpty) {
        properties =
            (jsonDecode(_propsJson.text) as Map).cast<String, dynamic>();
      }
      if (_itemPropsJson.text.trim().isNotEmpty) {
        itemProperties = (jsonDecode(_itemPropsJson.text) as List)
            .map((e) => (e as Map).cast<String, dynamic>())
            .toList();
      }

      B2Metric.instance.logEvent(
        name,
        properties: properties,
        itemProperties: itemProperties,
      );
      _addLog(TerminalLevel.app, 'logEvent("$name")');
    } catch (e) {
      _addLog(TerminalLevel.error, 'Send failed: $e');
    }
  }

  // ─── Render ────────────────────────────────────────────────────────────────

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF0F172A),
      body: SafeArea(
        child: Column(
          children: [
            Expanded(
              child: GestureDetector(
                onTap: () => FocusScope.of(context).unfocus(),
                child: SingleChildScrollView(
                  padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
                  keyboardDismissBehavior:
                      ScrollViewKeyboardDismissBehavior.onDrag,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Padding(
                        padding: EdgeInsets.only(top: 8, bottom: 8),
                        child: Text(
                          'B2Metric Testbed',
                          style: TextStyle(
                            fontSize: 20,
                            fontWeight: FontWeight.w700,
                            color: Color(0xFFF1F5F9),
                            letterSpacing: 0.3,
                          ),
                        ),
                      ),
                      _StatusBadge(ready: _sdkReady),
                      const SizedBox(height: 16),
                      _ConfigSection(
                        apiKey: _apiKey,
                        appIdentifier: _appIdentifier,
                        batchSize: _batchSize,
                        flushInterval: _flushInterval,
                        sessionTimeout: _sessionTimeout,
                        logLevel: _logLevel,
                        onLogLevel: (l) => setState(() => _logLevel = l),
                        sdkReady: _sdkReady,
                        onInit: _handleInit,
                        onFlush: _handleFlush,
                        onDestroy: _handleDestroy,
                      ),
                      const SizedBox(height: 12),
                      _PresetsSection(onPick: _fillPreset),
                      const SizedBox(height: 12),
                      _SendEventSection(
                        eventName: _eventName,
                        propsJson: _propsJson,
                        itemPropsJson: _itemPropsJson,
                        sdkReady: _sdkReady,
                        onSend: _handleSendEvent,
                        onClear: () {
                          _eventName.clear();
                          _propsJson.clear();
                          _itemPropsJson.clear();
                        },
                      ),
                    ],
                  ),
                ),
              ),
            ),
            _Terminal(
              logs: _logs,
              scrollController: _terminalScroll,
              onClear: () => setState(_logs.clear),
            ),
          ],
        ),
      ),
    );
  }
}

// ─── Sections ─────────────────────────────────────────────────────────────────

class _StatusBadge extends StatelessWidget {
  final bool ready;
  const _StatusBadge({required this.ready});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
      decoration: BoxDecoration(
        color: ready ? const Color(0xFF14532D) : const Color(0xFF1F2937),
        borderRadius: BorderRadius.circular(20),
      ),
      child: Text(
        ready ? '● SDK READY' : '○ NOT INITIALIZED',
        style: const TextStyle(
          fontSize: 12,
          fontWeight: FontWeight.w600,
          fontFamily: 'monospace',
          color: Color(0xFFD1FAE5),
        ),
      ),
    );
  }
}

class _SectionCard extends StatelessWidget {
  final String title;
  final Widget child;
  const _SectionCard({required this.title, required this.child});

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: const Color(0xFF1E293B),
        borderRadius: BorderRadius.circular(12),
      ),
      padding: const EdgeInsets.all(14),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.only(bottom: 12),
            child: Text(
              title.toUpperCase(),
              style: const TextStyle(
                fontSize: 11,
                fontWeight: FontWeight.w700,
                color: Color(0xFF475569),
                letterSpacing: 1.5,
              ),
            ),
          ),
          child,
        ],
      ),
    );
  }
}

class _FieldLabel extends StatelessWidget {
  final String text;
  const _FieldLabel(this.text);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 8, bottom: 4),
      child: Text(
        text,
        style: const TextStyle(fontSize: 11, color: Color(0xFF64748B)),
      ),
    );
  }
}

class _Input extends StatelessWidget {
  final TextEditingController controller;
  final String? hint;
  final bool multiline;
  final TextInputType? keyboardType;

  const _Input({
    required this.controller,
    this.hint,
    this.multiline = false,
    this.keyboardType,
  });

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: controller,
      keyboardType: keyboardType ??
          (multiline ? TextInputType.multiline : TextInputType.text),
      maxLines: multiline ? null : 1,
      minLines: multiline ? 3 : 1,
      autocorrect: false,
      enableSuggestions: false,
      style: const TextStyle(
        fontSize: 13,
        color: Color(0xFFE2E8F0),
        fontFamily: 'monospace',
      ),
      decoration: InputDecoration(
        isDense: true,
        filled: true,
        fillColor: const Color(0xFF0F172A),
        contentPadding:
            const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
        hintText: hint,
        hintStyle: const TextStyle(
          color: Color(0xFF4B5563),
          fontFamily: 'monospace',
          fontSize: 13,
        ),
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8),
          borderSide: const BorderSide(color: Color(0xFF334155)),
        ),
        enabledBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8),
          borderSide: const BorderSide(color: Color(0xFF334155)),
        ),
        focusedBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(8),
          borderSide: const BorderSide(color: Color(0xFF3B82F6)),
        ),
      ),
    );
  }
}

class _ActionBtn extends StatelessWidget {
  final String label;
  final Color color;
  final VoidCallback onPressed;
  final bool disabled;
  final double flex;

  const _ActionBtn({
    required this.label,
    required this.color,
    required this.onPressed,
    this.disabled = false,
    this.flex = 1,
  });

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: (flex * 100).round(),
      child: Padding(
        padding: const EdgeInsets.only(top: 10),
        child: Material(
          color: disabled ? const Color(0xFF374151) : color,
          borderRadius: BorderRadius.circular(8),
          child: InkWell(
            borderRadius: BorderRadius.circular(8),
            onTap: disabled ? null : onPressed,
            child: Padding(
              padding:
                  const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
              child: Center(
                child: Text(
                  label,
                  style: TextStyle(
                    fontSize: 14,
                    fontWeight: FontWeight.w600,
                    color:
                        disabled ? const Color(0xFF6B7280) : Colors.white,
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _ConfigSection extends StatelessWidget {
  final TextEditingController apiKey;
  final TextEditingController appIdentifier;
  final TextEditingController batchSize;
  final TextEditingController flushInterval;
  final TextEditingController sessionTimeout;
  final LogLevel logLevel;
  final ValueChanged<LogLevel> onLogLevel;
  final bool sdkReady;
  final VoidCallback onInit;
  final VoidCallback onFlush;
  final VoidCallback onDestroy;

  const _ConfigSection({
    required this.apiKey,
    required this.appIdentifier,
    required this.batchSize,
    required this.flushInterval,
    required this.sessionTimeout,
    required this.logLevel,
    required this.onLogLevel,
    required this.sdkReady,
    required this.onInit,
    required this.onFlush,
    required this.onDestroy,
  });

  @override
  Widget build(BuildContext context) {
    return _SectionCard(
      title: 'Configuration',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const _FieldLabel('API Key'),
          _Input(controller: apiKey, hint: 'your-api-key'),
          const _FieldLabel('App Identifier'),
          _Input(controller: appIdentifier, hint: 'your-app-identifier'),
          Row(
            children: [
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const _FieldLabel('Batch Size'),
                    _Input(
                      controller: batchSize,
                      keyboardType: TextInputType.number,
                    ),
                  ],
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const _FieldLabel('Flush Interval (s)'),
                    _Input(
                      controller: flushInterval,
                      keyboardType: TextInputType.number,
                    ),
                  ],
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const _FieldLabel('Session Timeout (min)'),
                    _Input(
                      controller: sessionTimeout,
                      keyboardType: TextInputType.number,
                    ),
                  ],
                ),
              ),
            ],
          ),
          const _FieldLabel('Log Level'),
          Wrap(
            spacing: 6,
            runSpacing: 6,
            children: _sdkLogLevels.map((l) {
              final active = l == logLevel;
              return InkWell(
                onTap: () => onLogLevel(l),
                borderRadius: BorderRadius.circular(6),
                child: Container(
                  padding: const EdgeInsets.symmetric(
                      horizontal: 10, vertical: 5),
                  decoration: BoxDecoration(
                    color: active
                        ? const Color(0xFF1D4ED8)
                        : const Color(0xFF0F172A),
                    border: Border.all(
                      color: active
                          ? const Color(0xFF3B82F6)
                          : const Color(0xFF334155),
                    ),
                    borderRadius: BorderRadius.circular(6),
                  ),
                  child: Text(
                    l.name,
                    style: TextStyle(
                      fontSize: 11,
                      fontWeight: FontWeight.w600,
                      fontFamily: 'monospace',
                      color: active
                          ? const Color(0xFFDBEAFE)
                          : const Color(0xFF64748B),
                    ),
                  ),
                ),
              );
            }).toList(),
          ),
          Row(
            children: sdkReady
                ? [
                    _ActionBtn(
                      label: 'Flush',
                      color: const Color(0xFF059669),
                      onPressed: onFlush,
                    ),
                    const SizedBox(width: 8),
                    _ActionBtn(
                      label: 'Destroy',
                      color: const Color(0xFFDC2626),
                      onPressed: onDestroy,
                    ),
                  ]
                : [
                    _ActionBtn(
                      label: 'Init SDK',
                      color: const Color(0xFF2563EB),
                      onPressed: onInit,
                    ),
                  ],
          ),
        ],
      ),
    );
  }
}

class _PresetsSection extends StatelessWidget {
  final ValueChanged<Preset> onPick;
  const _PresetsSection({required this.onPick});

  @override
  Widget build(BuildContext context) {
    return _SectionCard(
      title: 'Presets',
      child: Wrap(
        spacing: 6,
        runSpacing: 6,
        children: _presets.map((p) {
          return InkWell(
            onTap: () => onPick(p),
            borderRadius: BorderRadius.circular(6),
            child: Container(
              padding:
                  const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
              decoration: BoxDecoration(
                color: const Color(0xFF0F172A),
                border: Border.all(color: const Color(0xFF334155)),
                borderRadius: BorderRadius.circular(6),
              ),
              child: Text(
                p.label,
                style: const TextStyle(
                  fontSize: 12,
                  fontWeight: FontWeight.w500,
                  color: Color(0xFF94A3B8),
                  fontFamily: 'monospace',
                ),
              ),
            ),
          );
        }).toList(),
      ),
    );
  }
}

class _SendEventSection extends StatelessWidget {
  final TextEditingController eventName;
  final TextEditingController propsJson;
  final TextEditingController itemPropsJson;
  final bool sdkReady;
  final VoidCallback onSend;
  final VoidCallback onClear;

  const _SendEventSection({
    required this.eventName,
    required this.propsJson,
    required this.itemPropsJson,
    required this.sdkReady,
    required this.onSend,
    required this.onClear,
  });

  @override
  Widget build(BuildContext context) {
    return _SectionCard(
      title: 'Send Event',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const _FieldLabel('Event Name'),
          _Input(controller: eventName, hint: 'e.g. add_to_cart'),
          const _FieldLabel('properties (JSON object, optional)'),
          _Input(
            controller: propsJson,
            hint: '{ "price": 99, "currency": "USD" }',
            multiline: true,
          ),
          const _FieldLabel('itemProperties (JSON array, optional)'),
          _Input(
            controller: itemPropsJson,
            hint: '[{ "id": "SKU-1", "name": "Item", "price": 99 }]',
            multiline: true,
          ),
          Row(
            children: [
              _ActionBtn(
                label: 'Send Event',
                color: const Color(0xFF2563EB),
                onPressed: onSend,
                disabled: !sdkReady,
              ),
              const SizedBox(width: 8),
              _ActionBtn(
                label: 'Clear Form',
                color: const Color(0xFF475569),
                onPressed: onClear,
                flex: 0.55,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ─── Terminal ─────────────────────────────────────────────────────────────────

class _Terminal extends StatelessWidget {
  final List<LogEntry> logs;
  final ScrollController scrollController;
  final VoidCallback onClear;

  const _Terminal({
    required this.logs,
    required this.scrollController,
    required this.onClear,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 260,
      decoration: const BoxDecoration(
        color: Color(0xFF020617),
        border: Border(top: BorderSide(color: Color(0xFF1E293B))),
      ),
      child: Column(
        children: [
          Container(
            padding:
                const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
            decoration: const BoxDecoration(
              border:
                  Border(bottom: BorderSide(color: Color(0xFF1E293B))),
            ),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text(
                  'TERMINAL',
                  style: TextStyle(
                    fontSize: 10,
                    fontWeight: FontWeight.w700,
                    color: Color(0xFF475569),
                    letterSpacing: 2,
                    fontFamily: 'monospace',
                  ),
                ),
                InkWell(
                  onTap: onClear,
                  child: const Padding(
                    padding:
                        EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                    child: Text(
                      'CLEAR',
                      style: TextStyle(
                        fontSize: 10,
                        fontWeight: FontWeight.w600,
                        color: Color(0xFF3B82F6),
                        letterSpacing: 1,
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
          Expanded(
            child: logs.isEmpty
                ? const Center(
                    child: Text(
                      'Waiting for logs…',
                      style: TextStyle(
                        color: Color(0xFF1E293B),
                        fontSize: 12,
                        fontFamily: 'monospace',
                        fontStyle: FontStyle.italic,
                      ),
                    ),
                  )
                : ListView.builder(
                    controller: scrollController,
                    padding: const EdgeInsets.symmetric(
                        horizontal: 10, vertical: 6),
                    itemCount: logs.length,
                    itemBuilder: (ctx, i) {
                      final log = logs[i];
                      final color = _levelColors[log.level]!;
                      final levelLabel = log.level.name
                          .toUpperCase()
                          .padRight(5)
                          .substring(0, 5);
                      return Padding(
                        padding: const EdgeInsets.only(bottom: 1),
                        child: SelectableText.rich(
                          TextSpan(
                            style: const TextStyle(
                              fontSize: 11,
                              fontFamily: 'monospace',
                              height: 1.6,
                            ),
                            children: [
                              TextSpan(
                                text: '${log.time} ',
                                style: const TextStyle(
                                    color: Color(0xFF334155)),
                              ),
                              TextSpan(
                                text: '[$levelLabel] ',
                                style: TextStyle(
                                  color: color,
                                  fontWeight: FontWeight.w700,
                                ),
                              ),
                              TextSpan(
                                text: log.message,
                                style: TextStyle(color: color),
                              ),
                            ],
                          ),
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}
0
likes
140
points
227
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

B2Metric analytics SDK for Flutter. Collects events, manages sessions, and supports push notifications on Android and iOS.

Homepage

License

MIT (license)

Dependencies

flutter

More

Packages that depend on b2metric_sdk

Packages that implement b2metric_sdk