🎯 Learning Path:

  1. Complete REL-ID Complete Activation & Login Flow Codelab
  2. You are here → Push Notification Integration

Welcome to the REL-ID Push Notification Integration codelab! This tutorial enhances your existing REL-ID application with secure push notification capabilities using REL-ID SDK's setDeviceToken API.

What You'll Build

In this codelab, you'll enhance your existing REL-ID application with:

What You'll Learn

By completing this codelab, you'll master:

  1. REL-ID Push Architecture: Understanding two-channel security model vs standard push notifications
  2. setDeviceToken API: Complete implementation of REL-ID device token registration
  3. Device Token Management: Implementing token retrieval, registration, and refresh cycles
  4. Service Architecture: Building scalable push notification services with singleton patterns
  5. REL-ID SDK Integration: Best practices for integrating with existing REL-ID service layer
  6. Production Deployment: Security best practices, error handling, and monitoring strategies

Prerequisites

Before starting this codelab, ensure you have:

Get the Code from GitHub

The code to get started can be found in a GitHub repository.

You can clone the repository using the following command:

git clone https://github.com/uniken-public/codelab-flutter.git

Navigate to the relid-push-notification-token folder in the repository you cloned earlier

Why REL-ID Push Notifications?

REL-ID push notifications provide a secure, two-channel architecture that goes beyond standard push messaging with secure wake-up signals, MITM-proof channels, transaction approvals, and device-bound credentials.

REL-ID Flow: FCM Wake-up → App Launch → Secure REL-ID Channel → Encrypted Data Retrieval → User Action

Codelab Architecture Overview

This codelab implements two core components:

  1. PushNotificationService: Singleton service managing device tokens and REL-ID registration
  2. PushNotificationProvider: Flutter widget provider for automatic initialization

Before implementing push notifications, let's understand how REL-ID's secure notification system works.

REL-ID Two-Channel Security Model

REL-ID uses a sophisticated two-channel approach for maximum security:

📱 REL-ID Server → FCM/APNS (Wake-up Signal) → Mobile App → REL-ID Secure Channel → Encrypted Data

Channel 1: FCM Wake-Up Signal

Channel 2: REL-ID Secure Channel

Device Token Registration Flow

Here's how setDeviceToken() enables secure REL-ID communications:

// Device Registration Flow
FCM Token Generation → setDeviceToken(token) → REL-ID Backend Registration →
Secure Channel Establishment → Transaction Approval Capability

Step

Description

Security Benefit

1. FCM Token

Generate platform-specific device identifier

Device uniqueness

2. REL-ID Registration

setDeviceToken() registers device with REL-ID backend

Device-server binding

3. Secure Channel

Establish encrypted communication channel

MITM protection

4. Transaction Support

Enable approve/reject actions with MFA

Multi-factor security

REL-ID Push Notification Use Cases

Once integrated, your app can handle these secure notification types:

Firebase Role: Provides the platform infrastructure (FCM token generation)

REL-ID Role: Provides the secure communication and transaction approval capabilities

Now let's implement the core push notification service that handles FCM token management and REL-ID integration.

Enhance rdnaService with setDeviceToken

First, add the device token registration method to your existing REL-ID service:

// lib/uniken/services/rdna_service.dart (addition to existing class)

/// Registers device push notification token with REL-ID SDK
///
/// This method registers the device's FCM (Android) or APNS (iOS) push notification
/// token with the REL-ID SDK. The token is used by the backend to send push
/// notifications to this specific device. Unlike other REL-ID APIs, this method
/// is synchronous and doesn't trigger async events.
///
/// ## Parameters
/// - [deviceToken]: The FCM (Android) or APNS (iOS) device token string
///
/// ## Returns
/// RDNASyncResponse containing sync response (may have error or success)
///
/// ## Notes
/// - This API is synchronous and doesn't trigger async events
/// - Check response.error?.longErrorCode to determine success (0 = success)
/// - Should be called after successful FCM token retrieval
/// - Token should be re-registered if it changes (token refresh)
/// - Throws only on actual runtime errors (network, parsing, etc)
///
/// ## Example
/// ```dart
/// final token = await FirebaseMessaging.instance.getToken();
/// if (token != null) {
///   final response = await rdnaService.setDeviceToken(token);
///   if (response.error?.longErrorCode == 0) {
///     print('Device token registered successfully');
///   } else {
///     print('Token registration failed: ${response.error?.errorString}');
///   }
/// }
/// ```
Future<RDNASyncResponse> setDeviceToken(String deviceToken) async {
  print('RdnaService - Registering device push token with REL-ID SDK');
  print('RdnaService - Token length: ${deviceToken.length}');

  // Call plugin without redundant try-catch
  final response = await _rdnaClient.setDeviceToken(deviceToken);

  print('RdnaService - SetDeviceToken sync response received');
  print('RdnaService - Sync response:');
  print('  Long Error Code: ${response.error?.longErrorCode}');
  print('  Short Error Code: ${response.error?.shortErrorCode}');

  if (response.error?.longErrorCode == 0) {
    print('RdnaService - Device push token registration successful');
  } else {
    print('RdnaService - Device push token registration failed: ${response.error?.errorString}');
  }

  // Return response directly - caller decides what to do
  return response;
}

Create Push Notification Service

Now create the singleton service that manages all push notification functionality for both Android and iOS:

// lib/uniken/services/push_notification_service.dart

/// Push Notification Service
///
/// Cross-platform FCM integration for REL-ID SDK (Android & iOS).
/// Handles token registration with REL-ID backend via rdnaService.setDeviceToken().
///
/// ## Features
/// - Android & iOS FCM token retrieval and registration
/// - Android 13+ POST_NOTIFICATIONS permission handling
/// - iOS authorization via FirebaseMessaging (no native changes needed)
/// - Automatic token refresh handling
/// - REL-ID SDK integration via setDeviceToken()
///
/// ## iOS Requirements
/// - GoogleService-Info.plist must be added to ios/Runner/
/// - APNS certificate must be uploaded to Firebase Console
/// - FirebaseMessaging handles APNS delegate methods automatically
///
/// ## Usage
/// ```dart
/// final pushService = PushNotificationService.getInstance();
/// await pushService.initialize();
/// ```

import 'dart:io';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:permission_handler/permission_handler.dart';
import 'rdna_service.dart';

/// Push Notification Service
/// Cross-platform singleton for FCM token management (Android & iOS)
class PushNotificationService {
  static PushNotificationService? _instance;
  final RdnaService _rdnaService;
  bool _isInitialized = false;

  PushNotificationService._(this._rdnaService);

  /// Gets singleton instance
  static PushNotificationService getInstance() {
    if (_instance == null) {
      final rdnaService = RdnaService.getInstance();
      _instance = PushNotificationService._(rdnaService);
    }
    return _instance!;
  }

  /// Initialize FCM and register token with REL-ID SDK
  ///
  /// Supports both Android and iOS platforms. Handles permission requests,
  /// token retrieval, and automatic token refresh registration.
  ///
  /// ## Process
  /// 1. Ensure Firebase is initialized
  /// 2. Request permissions (Android 13+ and iOS)
  /// 3. Get and register initial token
  /// 4. Set up token refresh listener
  ///
  /// ## Throws
  /// - Exception if Firebase initialization fails
  /// - Exception if token retrieval fails
  Future<void> initialize() async {
    if (_isInitialized) {
      print('PushNotificationService - Already initialized');
      return;
    }

    print('PushNotificationService - Starting FCM initialization for ${Platform.operatingSystem}');

    // Ensure Firebase is initialized
    await _ensureFirebaseInitialized();

    // Request permissions (handles both Android and iOS)
    final hasPermission = await _requestPermissions();
    if (!hasPermission) {
      print('PushNotificationService - Permission not granted on ${Platform.operatingSystem}');
      return;
    }

    // Get and register initial token
    await _getAndRegisterToken();

    // Set up token refresh listener
    _setupTokenRefreshListener();

    _isInitialized = true;
    print('PushNotificationService - Initialization complete for ${Platform.operatingSystem}');
  }

  /// Ensure Firebase is initialized
  ///
  /// Firebase should auto-initialize from GoogleService-Info.plist (iOS) or
  /// google-services.json (Android). This method verifies initialization.
  ///
  /// ## Throws
  /// - Exception if Firebase fails to initialize
  Future<void> _ensureFirebaseInitialized() async {
    try {
      // Check if Firebase is already initialized
      if (Firebase.apps.isEmpty) {
        // Initialize Firebase if not already done
        await Firebase.initializeApp();
        print('PushNotificationService - Firebase initialized successfully');
      } else {
        print('PushNotificationService - Firebase already initialized');
      }
    } catch (e) {
      print('PushNotificationService - Firebase initialization failed: $e');
      rethrow;
    }
  }

  /// Request FCM permissions
  ///
  /// ## Android
  /// - Android 13+ (API 33+) requires POST_NOTIFICATIONS permission
  /// - Earlier versions don't need explicit permission
  ///
  /// ## iOS
  /// - Requests notification authorization (Alert, Sound, Badge)
  /// - Supports PROVISIONAL authorization (quiet notifications)
  ///
  /// ## Returns
  /// true if permission granted, false otherwise
  Future<bool> _requestPermissions() async {
    print('PushNotificationService - Platform OS: ${Platform.operatingSystem}');

    // Android 13+ requires POST_NOTIFICATIONS permission
    if (Platform.isAndroid) {
      print('PushNotificationService - Checking Android notification permission');
      final status = await Permission.notification.status;

      if (!status.isGranted) {
        print('PushNotificationService - Requesting POST_NOTIFICATIONS permission (Android 13+)');
        final result = await Permission.notification.request();
        print('PushNotificationService - POST_NOTIFICATIONS result: $result');

        if (!result.isGranted) {
          print('PushNotificationService - POST_NOTIFICATIONS permission denied');
          return false;
        }
      }
    }

    // Request FCM authorization (works for both Android and iOS)
    print('PushNotificationService - Requesting FCM authorization for ${Platform.operatingSystem}');

    final messaging = FirebaseMessaging.instance;
    final authStatus = await messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );

    print('PushNotificationService - FCM auth status: ${authStatus.authorizationStatus}');

    // iOS supports PROVISIONAL authorization (quiet notifications without prompt)
    final enabled = authStatus.authorizationStatus == AuthorizationStatus.authorized ||
                    authStatus.authorizationStatus == AuthorizationStatus.provisional;

    print('PushNotificationService - FCM permission: ${enabled ? "granted" : "denied"}');
    return enabled;
  }

  /// Get FCM token and register with REL-ID SDK
  ///
  /// ## Android
  /// Gets FCM registration token directly
  ///
  /// ## iOS
  /// Gets FCM token (mapped from APNS token by Firebase automatically)
  /// Checks APNS token availability first
  ///
  /// ## Throws
  /// - Exception if token retrieval fails
  Future<void> _getAndRegisterToken() async {
    print('PushNotificationService - Getting FCM token for ${Platform.operatingSystem}');

    final messaging = FirebaseMessaging.instance;

    // On iOS, check if APNS token is available first
    if (Platform.isIOS) {
      final apnsToken = await messaging.getAPNSToken();
      if (apnsToken != null) {
        print('PushNotificationService - iOS APNS token available, length: ${apnsToken.length}');
      } else {
        print('PushNotificationService - iOS APNS token not yet available, will retry via getToken()');
      }
    }

    final token = await messaging.getToken();
    if (token == null) {
      print('PushNotificationService - No FCM token received for ${Platform.operatingSystem}');
      return;
    }

    print('PushNotificationService - FCM token received for ${Platform.operatingSystem}, length: ${token.length}');
    print('PushNotificationService - FCM TOKEN: $token');

    // Register with REL-ID SDK (works for both Android FCM and iOS FCM tokens)
    final response = await _rdnaService.setDeviceToken(token);

    if (response.error?.longErrorCode == 0) {
      print('PushNotificationService - Token registered with REL-ID SDK successfully');
    } else {
      print('PushNotificationService - Token registration failed: ${response.error?.errorString}');
      throw Exception('Failed to register token with REL-ID SDK: ${response.error?.errorString}');
    }
  }

  /// Set up automatic token refresh
  ///
  /// Handles token refresh for both Android and iOS. When the token changes,
  /// automatically registers the new token with REL-ID SDK.
  void _setupTokenRefreshListener() {
    print('PushNotificationService - Setting up token refresh listener for ${Platform.operatingSystem}');

    FirebaseMessaging.instance.onTokenRefresh.listen((newToken) async {
      print('PushNotificationService - Token refreshed for ${Platform.operatingSystem}, length: ${newToken.length}');
      print('PushNotificationService - REFRESHED FCM TOKEN: $newToken');

      // Register new token with REL-ID SDK
      final response = await _rdnaService.setDeviceToken(newToken);

      if (response.error?.longErrorCode == 0) {
        print('PushNotificationService - Refreshed token registered with REL-ID SDK');
      } else {
        print('PushNotificationService - Token refresh registration failed: ${response.error?.errorString}');
      }
    }, onError: (error) {
      print('PushNotificationService - Token refresh error: $error');
    });
  }

  /// Get current FCM token (for debugging)
  ///
  /// Returns the current FCM token or null if not available.
  /// Works for both Android and iOS.
  ///
  /// ## Returns
  /// Current FCM token string or null
  Future<String?> getCurrentToken() async {
    final token = await FirebaseMessaging.instance.getToken();
    return token;
  }

  /// Cleanup (reset initialization state)
  void cleanup() {
    print('PushNotificationService - Cleanup');
    _isInitialized = false;
  }
}

Service Architecture Benefits

This implementation follows enterprise-grade patterns:

Pattern

Benefit

Implementation

Singleton

Single point of control

getInstance() factory method

Dependency Injection

Testable, maintainable

Constructor injection of rdnaService

Error Handling

Graceful failure management

Try-catch with logging

State Management

Prevents double initialization

_isInitialized flag

Platform Abstraction

Cross-platform compatibility

Platform OS checks

Now let's create a Flutter widget provider that automatically initializes push notifications when your app starts.

Create Push Notification Provider

Build a clean provider widget that integrates with your existing widget hierarchy:

// lib/uniken/providers/push_notification_provider.dart

/// Push Notification Provider Component
///
/// Simply initializes FCM on mount and lets the service handle everything.
/// This provider wraps the app and ensures FCM is initialized early in the
/// app lifecycle.
///
/// ## Usage
/// ```dart
/// runApp(
///   PushNotificationProvider(
///     child: MyApp(),
///   ),
/// );
/// ```

import 'package:flutter/material.dart';
import '../services/push_notification_service.dart';

class PushNotificationProvider extends StatefulWidget {
  final Widget child;

  const PushNotificationProvider({
    Key? key,
    required this.child,
  }) : super(key: key);

  @override
  State<PushNotificationProvider> createState() => _PushNotificationProviderState();
}

class _PushNotificationProviderState extends State<PushNotificationProvider> {
  @override
  void initState() {
    super.initState();
    _initializeFCM();
  }

  /// Initialize FCM on provider mount
  ///
  /// Calls the PushNotificationService singleton to initialize FCM.
  /// Errors are caught and logged but don't block app startup.
  Future<void> _initializeFCM() async {
    print('PushNotificationProvider - Initializing FCM');

    try {
      final pushService = PushNotificationService.getInstance();
      await pushService.initialize();
      print('PushNotificationProvider - FCM initialization successful');
    } catch (error) {
      print('PushNotificationProvider - FCM initialization failed: $error');
      // Don't block app startup on FCM initialization failure
    }
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

Update Provider Index

Add the new provider to your providers index:

// lib/uniken/providers/index.dart

export 'sdk_event_provider.dart';
export 'push_notification_provider.dart';

Integrate with App Widget Hierarchy

Update your main app entry point to include push notification initialization:

// lib/main.dart (integration example)

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'tutorial/navigation/app_router.dart';
import 'uniken/providers/sdk_event_provider.dart';
import 'uniken/providers/mtd_threat_provider.dart';
import 'uniken/providers/session_provider.dart';
import 'uniken/providers/push_notification_provider.dart';  // Add this import

void main() {
  runApp(
    const ProviderScope(
      child: PushNotificationProvider(  // Add this provider
        child: MyApp(),
      ),
    ),
  );
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return SDKEventProviderWidget(
      child: MaterialApp.router(
        title: 'REL-ID Tutorial',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2563EB)),
          useMaterial3: true,
        ),
        routerConfig: appRouter,
      ),
    );
  }
}

Provider Pattern Benefits

This approach provides several architectural advantages:

Benefit

Description

Implementation Detail

Automatic Initialization

Push notifications start immediately when app launches

initState with async initialization

Widget Integration

Fits naturally into existing widget hierarchy

Nested within existing providers

Error Isolation

Push notification failures don't crash the app

Try-catch in service layer

Development Friendly

No complex state management needed

Service handles all complexity

Production Ready

Graceful handling of permission denials and errors

Comprehensive error logging

Let's thoroughly test your push notification implementation with comprehensive scenarios to ensure production readiness.

Test Scenario 1: Complete Token Registration Flow

Prerequisites:

Test Steps:

  1. Launch Application
    flutter run
    # Or for specific platform:
    flutter run -d android
    flutter run -d ios
    
  2. Monitor Console Logs Look for this successful initialization sequence:
    ✅ PushNotificationProvider - Initializing FCM
    ✅ PushNotificationService - Starting FCM initialization for android
    ✅ PushNotificationService - POST_NOTIFICATIONS result: granted
    ✅ PushNotificationService - FCM token received, length: 142
    ✅ RdnaService - Device push token registration successful
    ✅ PushNotificationService - Initialization complete
    
  3. Verify Token Generation Confirm you see a valid FCM token logged (142+ character string)
  4. Test Token Refresh The refresh listener should also fire automatically:
    ✅ PushNotificationService - Token refreshed, length: 142
    ✅ RdnaService - Refreshed token registered with REL-ID SDK
    

Expected Results:

Test Scenario 2: Permission Handling

Android 13+ Permission Test:

  1. Install on Android 13+ device/emulator
  2. First launch - verify permission request appears
  3. Grant permission - verify FCM initialization continues
  4. Deny permission test - verify graceful handling

iOS Permission Test:

  1. Install on iOS device/simulator
  2. First launch - verify notification permission dialog appears
  3. Allow - verify FCM initialization continues
  4. Test provisional authorization - verify quiet notifications work

Expected Permission Flow:

📱 POST_NOTIFICATIONS permission request → User grants → FCM initialization
📱 POST_NOTIFICATIONS permission request → User denies → Graceful fallback

Test Scenario 3: Firebase Configuration Validation

Test Missing Configuration Files:

  1. Android: Temporarily rename google-services.json to google-services.json.backup
  2. iOS: Temporarily rename GoogleService-Info.plist to GoogleService-Info.plist.backup
  3. Build project: flutter run
  4. Verify error handling - should see Firebase related build errors
  5. Restore files and rebuild successfully

Test Google Services Plugin:

# Android: Verify Google Services plugin processing
cd android && ./gradlew app:dependencies | grep google-services

# iOS: Verify Firebase pods
cd ios && pod list | grep Firebase

Production Readiness Checklist

Before deploying to production, verify:

Congratulations! You've successfully implemented secure push notification integration with the REL-ID SDK. Here's your complete implementation overview.

🚀 What You've Built

Secure Device Registration - FCM tokens registered with REL-ID backend for two-channel security ✅ Firebase Integration - Complete FCM setup with Firebase auto-initialization ✅ Production-Ready Service - Singleton architecture with error handling and token refresh ✅ Cross-Platform Support - Android and iOS configuration with platform-specific permissions ✅ Provider Integration - Clean Flutter widget integration with existing app architecture

Key Files Created/Modified

lib/uniken/services/
├── push_notification_service.dart     ✅ FCM token management singleton
└── rdna_service.dart                  ✅ Enhanced with setDeviceToken()

lib/uniken/providers/
├── push_notification_provider.dart    ✅ Flutter widget provider
└── index.dart                         ✅ Updated exports

android/app/
├── build.gradle                       ✅ Google Services plugin
└── google-services.json               ✅ Firebase configuration

ios/Runner/
├── GoogleService-Info.plist           ✅ Firebase configuration (iOS)
└── Info.plist                         ✅ Push notification entitlements

pubspec.yaml                           ✅ Firebase dependencies
lib/main.dart                          ✅ Provider integration

Architecture Achievement

Your implementation demonstrates enterprise-grade patterns:

Component

Pattern

Benefit

PushNotificationService

Singleton

Centralized token management

PushNotificationProvider

Widget Provider

Automatic initialization

rdnaService Integration

Dependency Injection

Clean service layer

Firebase Configuration

Auto-initialization

Zero-configuration startup

Error Handling

Graceful Degradation

Production reliability

Security Benefits Unlocked

Your REL-ID push notification integration now enables:

Additional Resources

🎉 Congratulations!

You've successfully implemented secure push notification capabilities that integrate seamlessly with the REL-ID security ecosystem. Your app can now participate in secure, two-channel communications for transaction approvals, authentication challenges, and security notifications.

Your users now have a more secure, responsive authentication experience with the power of REL-ID's push notification infrastructure!