adk_notifier 0.0.2 copy "adk_notifier: ^0.0.2" to clipboard
adk_notifier: ^0.0.2 copied to clipboard

A Flutter SDK for the ART Real-time Notification Service, supporting history, live events, and push device registration.

example/lib/main.dart

import 'dart:convert';
import 'dart:io';

import 'package:art_adk/art_adk.dart';
import 'package:adk_notifier/adk_notifier.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ADK Notifications',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const NotificationsPage(),
    );
  }
}

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

  @override
  State<NotificationsPage> createState() => _NotificationsPageState();
}

class _NotificationsPageState extends State<NotificationsPage> {
  final List<String> _log = <String>[];

  final _recipientController = TextEditingController(text: 'USER_NAME');
  final _tokenController = TextEditingController(text: 'FCM_TOKEN');
  final _titleController = TextEditingController(text: 'Test notification');
  final _bodyController = TextEditingController(
    text: 'Sent from the Flutter example',
  );

  Adk? _adk;
  NotificationsApi? _notifications;
  Future<void> Function()? _removeLiveListener;
  List<ArtNotification> _items = <ArtNotification>[];
  String _status = 'Disconnected';
  String _platform = 'android';

  @override
  void initState() {
    super.initState();
    LocalNotificationService.instance.init(onTap: _handleNotificationTap);
  }

  void _handleNotificationTap(String? payload) {
    if (payload == null) return;
    final data = jsonDecode(payload) as Map<String, dynamic>;
    final id = data['id'] as String?;
    if (id != null) {
      _requireApi().markRead([id]);
    }
  }


  @override
  void dispose() {
    _removeLiveListener?.call();
    _adk?.disconnect();
    for (final controller in <TextEditingController>[
      _tokenController,
      _recipientController,
      _titleController,
      _bodyController,
    ]) {
      controller.dispose();
    }
    super.dispose();
  }

  Future<CredentialStore> _loadCredentials() async {
    try {
      final raw = await rootBundle.loadString('assets/adk-services.json');
      final json = jsonDecode(raw) as Map<String, dynamic>;
      return CredentialStore(
        environment: json['Environment'] as String? ?? '',
        projectKey: json['ProjectKey'] as String? ?? '',
        orgTitle: json['Org-Title'] as String? ?? '',
        clientID: json['Client-ID'] as String? ?? '',
        clientSecret: json['Client-Secret'] as String? ?? '',
      );
    } catch (e) {
      throw Exception('Failed to load assets/adk-services.json: $e');
    }
  }

  Future<String> _fetchPasscode(CredentialStore creds) async {
    final response = await http.post(
      Uri.parse('PASSCODE_ENDPOINT'),
      headers: <String, String>{
        'Client-Id': creds.clientID,
        'Client-Secret': creds.clientSecret,
        'X-Org': creds.orgTitle,
        'Environment': creds.environment,
        'ProjectKey': creds.projectKey,
        'Content-Type': 'application/json',
      },
      body: jsonEncode(<String, dynamic>{
        'username': 'USER_NAME',
        'first_name': 'USER_FIRST_NAME',
        'last_name': 'USER_LAST_NAME',
      }),
    );
    if (response.statusCode < 200 || response.statusCode >= 300) {
      throw Exception('passcode request failed (${response.statusCode})');
    }

    final decoded = jsonDecode(response.body) as Map<String, dynamic>;
    final data = decoded['data'];
    final passcode = data is Map<String, dynamic>
        ? data['passcode'] as String?
        : decoded['passcode'] as String?;
    if (passcode == null || passcode.isEmpty) {
      throw Exception('passcode missing in response');
    }
    return passcode;
  }

  Future<void> _connect() async {
    try {
      _logEvent('connecting...');
      final credentials = await _loadCredentials();
      final passcode = await _fetchPasscode(credentials);

      final updatedCredentials = credentials.copyWith(accessToken: passcode);

      final adk = Adk(
        adkConfig: AdkConfig(
          uri: 'YOUR_WEBSOCKET_URI',
          authToken: passcode,
          getCredentials: () => updatedCredentials,
        ),
      );

      adk.on('connection', (dynamic data) {
        if (data is ConnectionDetail) {
          _logEvent('connected · ${data.connectionId}');
        } else {
          _logEvent('connected · $data');
        }
      });
      adk.on('close', (dynamic reason) => _logEvent('closed · $reason'));

      await adk.connect();
      setState(() => _adk = adk);
      final api = NotificationsApi.fromAdk(adk,
        options: const NotificationsOptions(
          apiBaseUrl: "YOUR_BASE_URI",
          debug: true,
        ),
      );
      _logEvent('Initialized NotificationsApi...');
      _removeLiveListener = await api.onNew((notification) async {
        _logEvent(
          '[LIVE event] Received notification -> Id: "${notification.id}", Title: "${notification.title}"',
        );
        if (!mounted) return;
        setState(() {
          _items.removeWhere((e) => e.id == notification.id);
          _items.insert(0, notification);
        });

        try {
          await LocalNotificationService.instance.showWithData(
            title: notification.title,
            body: notification.body,
            data: {'id': notification.id},
          );
        } catch (e) {
          _logEvent('Local notification FAILED: $e');
        }

        _show('New notification: ${notification.title}');
      });
      setState(() {
        _adk = adk;
        _notifications = api;
        _status = 'Connected';
      });
      await _loadHistory();
    } catch (e) {
      _logEvent('connect failed · $e');
    }
  }

  void _logEvent(String message) {
    debugPrint(message);
    if (mounted) {
      setState(() {
        _log.add(
            '[${DateTime.now().toIso8601String().substring(11, 19)}] $message');
      });
    }
  }

  Future<void> _loadHistory() async {
    await _run('Loading history', () async {
      final result = await _requireApi().list();
      _logEvent(
        '[loadHistory] Received ${result.notifications.length} item(s) out of ${result.total} total.',
      );
      setState(() => _items = List<ArtNotification>.of(result.notifications)); // growable copy
      _show(
        'Loaded ${result.notifications.length} of ${result.total} notifications',
      );
    });
  }

  Future<void> _markAllRead() async {
    await _run(
      'Marking read',
          () async =>
          _show('Marked ${await _requireApi().markRead()} notifications read'),
    );
    final result = await _requireApi().list();
    setState(() => _items = List<ArtNotification>.of(result.notifications));
  }

  Future<void> _send() async {
    await _run('Sending', () async {
      _logEvent(
        'Sending',
      );
      final result = await _requireApi().send(
        SendInput(
          recipients: _recipientController.text
              .split(',')
              .map((value) => value.trim())
              .where((value) => value.isNotEmpty)
              .toList(),
          type: 'example.test',
          title: _titleController.text,
          body: _bodyController.text,
          channels: [
            "in_app",
            "push",
          ],
        ),
      );
      await _loadHistory();
      _show(
        'Created ${result.created}; skipped ${result.skipped}',
      );
    });
  }

  Future<void> _registerDevice() async {
    await _run('Registering device', () async {
      final device = await _requireApi().registerDevice(
        RegisterDeviceInput(
          token: _tokenController.text.trim(),
          platform: Platform.isAndroid
              ? "android"
              : "ios",
        ),
      );
      _show(
        device == null
            ? 'No device returned'
            : 'Registered ${device.platform} device',
      );
    });
  }

  Future<void> _run(String busyText, Future<void> Function() action) async {
    setState(() => _status = busyText);
    try {
      await action();
    } catch (error) {
      _show('Error: $error');
      setState(() => _status = 'Error');
    }
  }

  NotificationsApi _requireApi() =>
      _notifications ?? (throw StateError('Connect first'));

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ADK Notifications example')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: <Widget>[
          const Text(
            'Connection',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 28),
       FilledButton.icon(
            onPressed: _connect,
            icon: const Icon(Icons.link),
            label: const Text('Connect'),
          ),
          const SizedBox(height: 8),
          Text(_status),
          const Divider(height: 32),
          const Text(
            'History and actions',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: <Widget>[
              OutlinedButton(
                onPressed: _loadHistory,
                child: const Text('Load history'),
              ),
              OutlinedButton(
                onPressed: _markAllRead,
                child: const Text('Mark all read'),
              ),
            ],
          ),
          const SizedBox(height: 28),
          _field(_recipientController, 'Recipients (comma separated)'),
          _field(_titleController, 'Title'),
          _field(_bodyController, 'Body'),
          const SizedBox(height: 28),
          FilledButton(
            onPressed: _send,
            child: const Text('Send test notification'),
          ),
          const Divider(height: 32),
          const Text(
            'Push device',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          _field(_tokenController, 'FCM token'),
          DropdownButtonFormField<String>(
            initialValue: _platform,
            decoration: const InputDecoration(labelText: 'Platform'),
            items: const <String>['android', 'ios', 'web']
                .map(
                  (value) => DropdownMenuItem(value: value, child: Text(value)),
            )
                .toList(),
            onChanged: (value) =>
                setState(() => _platform = value ?? 'android'),
          ),
          FilledButton.tonal(
            onPressed: _registerDevice,
            child: const Text('Register device'),
          ),
          const SizedBox(height: 20),
          Text(
            'Notifications (${_items.length})',
            style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          if (_items.isEmpty)
            const Padding(
              padding: EdgeInsets.symmetric(vertical: 24),
              child: Text('No notifications loaded yet.'),
            ),
          ..._items.map(
                (item) =>
                    Card(color: item.status == 'unread'
                        ? Colors.blue.shade50
                        : null,
                      child: ListTile(
                        title: Text(item.title),
                        subtitle: Text(item.body),
                          onTap: () async {
                            await _requireApi().markRead([item.id]);

                            setState(() {
                              final index = _items.indexWhere((e) => e.id == item.id);
                              if (index != -1) {
                                _items[index] = ArtNotification(
                                  id: item.id,
                                  type: item.type,
                                  title: item.title,
                                  body: item.body,
                                  createdAt: item.createdAt,
                                  data: item.data,
                                  status: 'read',
                                );
                              }
                            });
                          },
                        leading: Icon(
                          item.status == 'unread'
                              ? Icons.notifications_active
                              : Icons.notifications_none,
                        ),
                      ),
                    )

          ),
        ],
      ),
    );
  }

  Widget _field(
      TextEditingController controller,
      String label, {
        bool secret = false,
      }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: TextField(
        controller: controller,
        obscureText: secret,
        decoration: InputDecoration(
          labelText: label,
          border: const OutlineInputBorder(),
        ),
      ),
    );
  }
}




/// Called when the user taps a notification while the app is running
/// (foreground or backgrounded but still alive).
typedef NotificationTapCallback = void Function(String? payload);
@pragma('vm:entry-point')
void _onBackgroundNotificationTap(NotificationResponse response) {
  debugPrint('[LocalNotificationService] Background tap: ${response.payload}');
}


class LocalNotificationService {
  LocalNotificationService._();

  static final LocalNotificationService instance = LocalNotificationService._();

  final FlutterLocalNotificationsPlugin _plugin =
  FlutterLocalNotificationsPlugin();

  static const AndroidNotificationChannel _channel = AndroidNotificationChannel(
    'adk_notifier',
    'App notifications',
    description: 'Notifications from the ADK Notifications API',
    importance: Importance.high,
  );

  bool _initialized = false;
  NotificationTapCallback? _onTap;

  /// Initializes the plugin, creates the Android notification channel, and
  /// requests notification permissions on both platforms. Safe to call
  /// multiple times — subsequent calls are no-ops.

  Future<void> init({NotificationTapCallback? onTap}) async {
    if (_initialized) return;
    _onTap = onTap;

    const AndroidInitializationSettings androidSettings =
    AndroidInitializationSettings('@mipmap/ic_launcher');

    const DarwinInitializationSettings iosSettings = DarwinInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: true,
    );

    await _plugin.initialize(
      settings: const InitializationSettings(
        android: androidSettings,
        iOS: iosSettings,
      ),
      onDidReceiveNotificationResponse: (NotificationResponse response) {
        _onTap?.call(response.payload);
      },
      onDidReceiveBackgroundNotificationResponse: _onBackgroundNotificationTap,
    );

    await _plugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(_channel);

    final granted = await _plugin.resolvePlatformSpecificImplementation
    <AndroidFlutterLocalNotificationsPlugin>()
        ?.requestNotificationsPermission();
    debugPrint('[LocalNotificationService] Android permission granted: $granted');

    await _plugin
        .resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
        ?.requestPermissions(alert: true, badge: true, sound: true);

    _initialized = true;
  }

  /// Shows a notification in the system tray.
  Future<void> show({
    required String title,
    required String body,
    String? payload,
    int? id,
  }) async {
    if (!_initialized) {
      throw StateError(
        'LocalNotificationService.show() called before init(). '
            'Call LocalNotificationService.instance.init() first, '
            'typically in initState().',
      );
    }

    try {
      await _plugin.show(
        id: id ?? DateTime.now().millisecondsSinceEpoch ~/ 1000,
        title: title,
        body: body,
        notificationDetails: NotificationDetails(
          android: AndroidNotificationDetails(
            _channel.id,
            _channel.name,
            channelDescription: _channel.description,
            importance: Importance.high,
            priority: Priority.high,
            icon: '@mipmap/ic_launcher',
          ),
          iOS: const DarwinNotificationDetails(
            presentAlert: true,
            presentBadge: true,
            presentSound: true,
          ),
        ),
        payload: payload,
      );
      debugPrint('[LocalNotificationService] show() completed');
    } catch (e, st) {
      debugPrint('[LocalNotificationService] show() FAILED: $e\n$st');
    }
  }

  /// Convenience helper for showing a notification whose payload is a JSON
  /// map — the common case of round-tripping an id back through [init]'s
  /// `onTap` callback.
  Future<void> showWithData({
    required String title,
    required String body,
    required Map<String, dynamic> data,
    int? id,
  }) {
    return show(title: title, body: body, payload: jsonEncode(data), id: id);
  }
}
1
likes
160
points
102
downloads

Documentation

Documentation
API reference

Publisher

verified publisherarealtimetech.com

Weekly Downloads

A Flutter SDK for the ART Real-time Notification Service, supporting history, live events, and push device registration.

Homepage

Topics

#notifications #push-notifications #websocket #realtime #art

License

MIT (license)

Dependencies

art_adk, flutter, http

More

Packages that depend on adk_notifier