🎯 Learning Path:
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.
In this codelab, you'll enhance your existing REL-ID application with:
setDeviceToken APIBy completing this codelab, you'll master:
Before starting this codelab, ensure you have:
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
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
This codelab implements two core components:
Before implementing push notifications, let's understand how REL-ID's secure notification system works.
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
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 |
| Device-server binding |
3. Secure Channel | Establish encrypted communication channel | MITM protection |
4. Transaction Support | Enable approve/reject actions with MFA | Multi-factor security |
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.
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;
}
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;
}
}
This implementation follows enterprise-grade patterns:
Pattern | Benefit | Implementation |
Singleton | Single point of control |
|
Dependency Injection | Testable, maintainable | Constructor injection of |
Error Handling | Graceful failure management | Try-catch with logging |
State Management | Prevents double initialization |
|
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.
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;
}
}
Add the new provider to your providers index:
// lib/uniken/providers/index.dart
export 'sdk_event_provider.dart';
export 'push_notification_provider.dart';
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,
),
);
}
}
This approach provides several architectural advantages:
Benefit | Description | Implementation Detail |
Automatic Initialization | Push notifications start immediately when app launches |
|
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.
Prerequisites:
google-services.json (Android) and GoogleService-Info.plist (iOS)Test Steps:
flutter run
# Or for specific platform:
flutter run -d android
flutter run -d ios
✅ 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
✅ PushNotificationService - Token refreshed, length: 142
✅ RdnaService - Refreshed token registered with REL-ID SDK
Expected Results:
Android 13+ Permission Test:
iOS Permission Test:
Expected Permission Flow:
📱 POST_NOTIFICATIONS permission request → User grants → FCM initialization
📱 POST_NOTIFICATIONS permission request → User denies → Graceful fallback
Test Missing Configuration Files:
google-services.json to google-services.json.backupGoogleService-Info.plist to GoogleService-Info.plist.backupflutter runTest 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
Before deploying to production, verify:
google-services.json from production project in android/app/GoogleService-Info.plist from production project in ios/Runner/Congratulations! You've successfully implemented secure push notification integration with the REL-ID SDK. Here's your complete implementation overview.
✅ 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
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
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 |
Your REL-ID push notification integration now enables:
🎉 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!