SDK do Flutter - Suporte a notificações push

Documento

Suporte a notificações push

Acompanhe as interações do usuário com notificações push para medir campanhas de reengajamento e atribuir conversões com precisão, integrando o Firebase Cloud Messaging (FCM) com o Singular SDK.

Siga as diretrizes de implementação abaixo para garantir que os dados de notificação sejam passados corretamente para o Singular SDK para atribuição adequada.

Por que rastrear notificações push: As notificações push impulsionam o reengajamento, mas o rastreamento requer a integração correta. Singular garante que os usuários que interagem com as notificações sejam atribuídos corretamente, otimizando campanhas de marketing e estratégias de engajamento.


Guia de implementação

Configurar o Firebase Cloud Messaging

Instale os pacotes do Firebase e defina configurações específicas da plataforma para suporte a notificações push em seu aplicativo Flutter.

Instalar os pacotes do Firebase

Adicione as dependências do Firebase ao seu arquivo pubspec.yaml para obter a funcionalidade principal e o suporte a mensagens.

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.24.2
  firebase_messaging: ^14.7.10
  singular_flutter_sdk: ^1.8.0

Depois de adicionar as dependências, execute flutter pub get para instalar os pacotes.


Configuração do iOS

Registre seu aplicativo iOS no Firebase e configure os recursos de notificação por push no Xcode.

  1. Registre o aplicativo iOS: Crie um aplicativo iOS no seu projeto do Firebase Console
  2. Adicionar arquivo de configuração: Baixe GoogleService-Info.plist e adicione-o à pasta Xcode Runner
  3. Habilitar recursos: Nas configurações do projeto Xcode, ative o recurso Notificações por push
  4. Habilitar modos de segundo plano: Habilite os Modos em segundo plano e marque Notificações remotas

Configuração do Android

Registre seu aplicativo Android no Firebase e adicione o arquivo de configuração ao seu projeto.

  1. Registar a aplicação Android: Crie um aplicativo Android no seu projeto do Firebase Console
  2. Adicionar arquivo de configuração: Baixe google-services.json e coloque-o em android/app/
  3. Verificar dependências: Verifique se as dependências de mensagens do Firebase foram adicionadas e se as permissões foram concedidas em AndroidManifest.xml

Inicializar o Firebase no Flutter

Configure o Firebase para inicializar antes de executar seu aplicativo Flutter e configure o manipulador de mensagens em segundo plano para notificações recebidas quando o aplicativo não estiver em primeiro plano.

Dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

// Background message handler (must be top-level function)
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print('Background message: ${message.messageId}');
  print('Data: ${message.data}');
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase
  await Firebase.initializeApp();

  // Set background message handler
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(MyApp());
}

Configurar caminhos de link de envio

Defina os caminhos JSON onde os links de rastreamento Singular estão localizados na estrutura de carga útil da notificação por push.

Configure os caminhos de link de push passando matrizes de cadeias de caracteres que especificam o caminho principal para o link Singular em sua estrutura de dados de notificação. Cada caminho é uma matriz que representa a estrutura aninhada de chaves.

Dart
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';

SingularConfig config = SingularConfig(
  'YOUR_SDK_KEY',
  'YOUR_SDK_SECRET'
);

// Configure paths where Singular links are located in push payload
config.pushNotificationsLinkPaths = [
  ['sng_link'],                              // Top-level key
  ['path', 'to', 'url'],                     // Nested path
  ['rootObj', 'nestedObj', 'singularLink']   // Deep nested path
];

Singular.start(config);

Exemplos de configuração de caminho:

  • Chaves simples: Use ['sng_link'] para chaves de nível superior no payload
  • Chaves aninhadas: Use ['rootObj', 'nestedObj', 'key'] para percorrer estruturas JSON aninhadas
  • Vários caminhos: Defina várias matrizes de caminho para verificar diferentes locais possíveis para links Singulares

Propriedade de configuração:

List<List<String>>? pushNotificationsLinkPaths

Para obter a documentação de configuração completa, consulte a referência pushNotificationsLinkPaths.


Manipulação específica da plataforma

Manipulação de notificações push no Flutter

Implemente ouvintes de mensagens do Firebase para capturar dados de notificação em estados de primeiro e segundo plano e, em seguida, passe os dados para Singular para rastreamento de atribuição.

Dart
import 'package:flutter/material.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:singular_flutter_sdk/singular.dart';

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

  @override
  void initState() {
    super.initState();
    setupPushNotifications();
  }

  void setupPushNotifications() async {
    // Request permission for iOS
    NotificationSettings settings = await _firebaseMessaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );

    print('User granted permission: ${settings.authorizationStatus}');

    // Handle foreground notifications
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Foreground message received: ${message.messageId}');
      handleForegroundNotification(message);
    });

    // Handle notifications that opened the app from background
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print('Notification opened app from background: ${message.messageId}');
      handleBackgroundNotification(message);
    });

    // Check for notification that launched the app (terminated state)
    RemoteMessage? initialMessage = await _firebaseMessaging.getInitialMessage();
    if (initialMessage != null) {
      print('App launched from notification: ${initialMessage.messageId}');
      handleTerminatedNotification(initialMessage);
    }
  }

  void handleForegroundNotification(RemoteMessage message) {
    String title = message.notification?.title ?? '';
    String body = message.notification?.body ?? '';
    Map<String, dynamic> data = message.data;

    print('Title: $title');
    print('Body: $body');
    print('Data: $data');

    // Pass notification data to Singular
    if (data.isNotEmpty) {
      Singular.handlePushNotification(data);
    }

    // Display local notification or custom UI
    displayLocalNotification(title, body, data);
  }

  void handleBackgroundNotification(RemoteMessage message) {
    print('Processing background notification: ${message.messageId}');

    // Pass notification data to Singular
    if (message.data.isNotEmpty) {
      Singular.handlePushNotification(message.data);
    }

    // Navigate to specific screen based on notification data
    navigateFromNotification(message.data);
  }

  void handleTerminatedNotification(RemoteMessage message) {
    print('Processing terminated state notification: ${message.messageId}');

    // Pass notification data to Singular
    if (message.data.isNotEmpty) {
      Singular.handlePushNotification(message.data);
    }

    // Navigate to specific screen
    navigateFromNotification(message.data);
  }

  void displayLocalNotification(
    String title, 
    String body, 
    Map<String, dynamic> data
  ) {
    // Your notification display logic
    print('Displaying notification: $title - $body');
  }

  void navigateFromNotification(Map<String, dynamic> data) {
    // Your navigation logic based on notification data
    final route = data['route'];
    print('Navigating to: $route');
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

Assinatura do método:

static void handlePushNotification(Map<String, dynamic> notificationData)

Para obter a documentação completa do método, consulte a referência handlePushNotification.


Configuração nativa do iOS

Aplicativo em estado terminado

Configure seu AppDelegate do iOS para passar opções de inicialização para o SDK Singular para rastreamento automático de push quando o aplicativo for aberto a partir de um estado finalizado.

Em AppDelegate.m ou AppDelegate.swift, passe as opções de lançamento para o SDK Singular:

Implementação Objective-C

AppDelegate.m
// Import at the top of the file
#import "SingularAppDelegate.h"

- (BOOL)application:(UIApplication *)application 
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [GeneratedPluginRegistrant registerWithRegistry:self];

    // Pass launch options to Singular for push tracking
    [SingularAppDelegate shared].launchOptions = launchOptions;

    return [super application:application 
        didFinishLaunchingWithOptions:launchOptions];
}

Implementação Swift

AppDelegate.swift
import singular_flutter_sdk

override func application(_ application: UIApplication, 
    didFinishLaunchingWithOptions 
    launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    // Pass launch options to Singular for push tracking
    if let singularAppDelegate = SingularAppDelegate.shared() {
        singularAppDelegate.launchOptions = launchOptions
    }

    return super.application(application, 
        didFinishLaunchingWithOptions: launchOptions)
}

Tratamento automático: Quando os usuários tocam nas notificações push enquanto seu aplicativo não está em execução, o Singular captura automaticamente o payload da notificação durante a inicialização do aplicativo por meio das opções de inicialização.


Configuração nativa do Android

Aplicativo em segundo ou primeiro plano

Configure sua MainActivity do Android para passar intenções de notificação para o SDK Singular quando o aplicativo estiver em estados de segundo ou primeiro plano.

Em sua MainActivity, substitua onNewIntent para passar intents para o Singular:

Implementação Java

MainActivity.java
// Add imports at the top
import android.content.Intent;
import com.singular.flutter_sdk.SingularBridge;

// Add to MainActivity class
@Override
public void onNewIntent(Intent intent) {
    if(intent.getData() != null) {
        setIntent(intent);
        super.onNewIntent(intent);

        // Pass intent to Singular for push tracking
        SingularBridge.onNewIntent(intent);
    }
}

Implementação Kotlin

MainActivity.kt
// Add imports at the top
import android.content.Intent
import com.singular.flutter_sdk.SingularBridge

// Add to MainActivity class
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    if (intent.data != null) {
        setIntent(intent)

        // Pass intent to Singular for push tracking
        SingularBridge.onNewIntent(intent)
    }
}

Aplicativo em estado terminado

Nenhuma configuração adicional é necessária para aplicativos Android em estado terminado. A camada de ponte do Flutter lida com esse cenário automaticamente quando os usuários tocam nas notificações.

Tratamento automático: Quando os utilizadores tocam nas notificações push enquanto a sua aplicação não está em execução, o Singular captura automaticamente os dados da notificação através da integração da bridge nativa.


Guia de Validação

Verificar carga útil na sessão inicial

Confirme que os links das notificações push são passados corretamente para o Singular inspecionando a chamada da API de início de sessão.

O SDK do Singular inclui a carga útil da notificação push no parâmetro singular_link na solicitação de sessão inicial quando os usuários tocam nas notificações.

Exemplo de solicitação de início de sessão:

https://sdk-api-v1.singular.net/api/v1/start?
a=<SDK-Key>
&singular_link=https://singularassist2.sng.link/C4nw9/r1m0?_dl=singular%3A%2F%2Ftest&_smtype=3
&i=net.singular.sampleapp
&s=1740905574084
&sdk=Singular/Flutter-v1.8.0

Verificação alternativa: Use o Console do SDK Singular para verificar o rastreamento de notificações por push. Verifique o campo URL do Deeplink para confirmar que o link de rastreamento foi capturado corretamente.


Configuração avançada

Configuração de domínio ESP

Configure domínios externos se você envolver links do Singular dentro do Provedor de serviços de e-mail (ESP) ou outros domínios de terceiros.

Dart
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';

// Configure ESP domains for wrapped Singular links
SingularConfig config = SingularConfig(
  'YOUR_SDK_KEY',
  'YOUR_SDK_SECRET'
);

config.espDomains = ['sl.esp.link', 'custom.domain.com'];

Singular.start(config);

Propriedade de configuração:

List<String>? espDomains

Segurança Observação: Por padrão, somente domínios sng.linkpredefinidos na página Gerenciar links do Singular são permitidos. Configure domínios ESP explicitamente se estiver usando links agrupados.

Para obter a documentação de configuração completa, consulte a referência espDomains.


Roteamento dinâmico de deep link

Implemente vários destinos de deep link a partir de uma única notificação, configurando um link de rastreamento Singular com substituições de redirecionamento dinâmico.

Exemplo de caso de uso: Uma notificação de notícias de última hora com várias opções de ação

  • Ler as últimas notícias: newsapp://article?id=12345
  • Tópicos em destaque: newsapp://trending
  • Desporto: newsapp://sports

Em vez de criar vários links de rastreamento, use um link Singular e substitua os redirecionamentos dinamicamente com base na seleção do usuário. Consulte Substituição de redirecionamentos em links de rastreamento singulares para obter detalhes de implementação.


Exemplo de implementação completa

Implementação abrangente de notificação push com configuração do Firebase, configuração Singular e manipuladores específicos da plataforma para aplicativos Flutter.

Dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';

// Background message handler (top-level function)
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print('Background message: ${message.messageId}');

  // Singular handles background notifications automatically
  if (message.data.isNotEmpty) {
    print('Background notification data: ${message.data}');
  }
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase
  await Firebase.initializeApp();

  // Set background message handler
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

  @override
  void initState() {
    super.initState();
    initializeSingular();
    setupPushNotifications();
  }

  void initializeSingular() {
    // Configure Singular SDK
    SingularConfig config = SingularConfig(
      'YOUR_SDK_KEY',
      'YOUR_SDK_SECRET'
    );

    // Configure push link paths
    config.pushNotificationsLinkPaths = [
      ['sng_link'],
      ['data', 'url'],
      ['rootObj', 'nestedObj', 'singularLink']
    ];

    // Configure ESP domains if needed
    config.espDomains = ['sl.esp.link'];

    // Enable logging for debugging
    config.enableLogging = true;

    // Initialize SDK
    Singular.start(config);
  }

  void setupPushNotifications() async {
    // Request permission (iOS)
    NotificationSettings settings = await _firebaseMessaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );

    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      print('User granted permission');
    }

    // Get FCM token
    String? token = await _firebaseMessaging.getToken();
    print('FCM Token: $token');

    // Handle foreground notifications
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Foreground notification: ${message.messageId}');
      handleForegroundNotification(message);
    });

    // Handle notification that opened app from background
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print('Notification opened app: ${message.messageId}');
      handleBackgroundNotification(message);
    });

    // Check for notification that launched the app
    RemoteMessage? initialMessage = await _firebaseMessaging.getInitialMessage();
    if (initialMessage != null) {
      print('App launched from notification: ${initialMessage.messageId}');
      handleTerminatedNotification(initialMessage);
    }
  }

  void handleForegroundNotification(RemoteMessage message) {
    final title = message.notification?.title ?? '';
    final body = message.notification?.body ?? '';
    final data = message.data;

    print('Foreground - Title: $title, Body: $body');

    // Pass notification data to Singular
    if (data.isNotEmpty) {
      Singular.handlePushNotification(data);
    }

    // Display notification to user
    showNotificationDialog(title, body, data);
  }

  void handleBackgroundNotification(RemoteMessage message) {
    print('Processing background notification');

    // Pass notification data to Singular
    if (message.data.isNotEmpty) {
      Singular.handlePushNotification(message.data);
    }

    // Navigate based on notification data
    navigateFromNotification(message.data);
  }

  void handleTerminatedNotification(RemoteMessage message) {
    print('Processing terminated state notification');

    // Pass notification data to Singular
    if (message.data.isNotEmpty) {
      Singular.handlePushNotification(message.data);
    }

    // Navigate based on notification data
    navigateFromNotification(message.data);
  }

  void showNotificationDialog(
    String title, 
    String body, 
    Map<String, dynamic> data
  ) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(body),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
              navigateFromNotification(data);
            },
            child: Text('Open'),
          ),
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text('Dismiss'),
          ),
        ],
      ),
    );
  }

  void navigateFromNotification(Map<String, dynamic> data) {
    // Parse notification data for routing
    final route = data['route'];
    final productId = data['product_id'];

    print('Navigating to: $route');

    // Navigate to appropriate screen
    if (route == 'product' && productId != null) {
      navigatorKey.currentState?.pushNamed('/product/$productId');
    } else if (route == 'promo') {
      navigatorKey.currentState?.pushNamed('/promo');
    } else {
      navigatorKey.currentState?.pushNamed('/');
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      navigatorKey: navigatorKey,
      initialRoute: '/',
      routes: {
        '/': (context) => HomeScreen(),
        '/product': (context) => ProductScreen(),
        '/promo': (context) => PromoScreen(),
      },
    );
  }
}

// Placeholder screens
class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Center(child: Text('Home Screen')),
    );
  }
}

class ProductScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Product')),
      body: Center(child: Text('Product Screen')),
    );
  }
}

class PromoScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Promo')),
      body: Center(child: Text('Promo Screen')),
    );
  }
}

Considerações importantes

Notas de implementação

  • Nenhum manipulador de retorno de chamada: Ao contrário de singularLinksHandler, o recurso de notificação push não fornece retornos de chamada de carga. Implemente a sua própria lógica de ligação profunda para encaminhar os utilizadores para conteúdos específicos dentro da sua aplicação
  • Fluxo de atribuição: quando os usuários tocam nas notificações, o Singular recupera o payload e o inclui no evento de início de sessão acionado pela inicialização do SDK. O backend processa esses dados para atribuir o ponto de contato da notificação push e registrar o rastreamento de reengajamento
  • Restrições de domínio: Por padrão, somente domínios de link único (sng.link) da página Gerenciar links são permitidos. Configure explicitamente os domínios ESP para links agrupados usando espDomains
  • Diferenças de plataforma: o iOS requer a configuração do AppDelegate para o estado terminado, enquanto o Android lida com isso automaticamente através do módulo de ponte
  • Testes: Ativar o registo SDK durante o desenvolvimento para verificar se os dados de notificação push são corretamente capturados e processados

Sucesso: Seguindo essas etapas, seu aplicativo agora rastreia as interações de notificação por push com o Singular, melhorando os insights de desempenho da campanha e garantindo uma atribuição precisa de reengajamento.