nemu_tracking_flutter 1.4.0
nemu_tracking_flutter: ^1.4.0 copied to clipboard
Nemu Smart Links attribution SDK for Flutter - UTM tracking, deep linking, and install attribution.
example/lib/main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:nemu_tracking_flutter/nemu_tracking_flutter.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Nemu SDK Example',
theme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String _status = 'Initializing...';
final _clickIdController = TextEditingController();
String? _activeClickId;
bool _loadingHistory = false;
@override
void initState() {
super.initState();
_activeClickId = getInstallReferrerClickIdOverride();
_initSdk();
}
void _initSdk() {
try {
NemuTracking.instance.init(
const NemuInitParams(
apiKey: 'deb2b453-8e0c-4edc-9980-45f599397e4d',
// `uriScheme` é opcional; omita para apps que usam só Universal Links.
// Deve ser o mesmo scheme declarado no AndroidManifest.xml
// (`<data android:scheme="nemuexample"/>`) — o valor anterior
// (`nemu.fluttersdk.example://`) não correspondia a nenhum intent
// filter, então o app nunca recebia deep links por URI scheme.
uriScheme: 'nemuexample',
trackingId: 'VaCItBH8b4',
isDebugMode: true
),
);
setState(() => _status = 'SDK loaded successfully');
NemuTracking.instance.onDeepLink((data) {
debugPrint('Deep link data: $data');
});
} catch (error) {
setState(() => _status = 'Error: $error');
}
}
void _handleSetClickId() {
final trimmed = _clickIdController.text.trim();
if (trimmed.isEmpty) return;
setInstallReferrerClickIdOverride(trimmed);
setState(() => _activeClickId = trimmed);
}
void _handleClearClickId() {
setInstallReferrerClickIdOverride(null);
setState(() {
_activeClickId = null;
_clickIdController.clear();
});
}
/// A chamada espera o rastreamento do launch antes de decidir que não há
/// histórico (ver `_awaitLaunchTracking`), então numa primeira abertura pode
/// levar alguns segundos — daí o estado de carregamento.
///
/// `ensureInitialized()` roda de forma síncrona dentro de
/// `getLastSessionHistory()`, então um `init()` que falhou chega aqui como
/// `StateError` e não como future rejeitado; o `try` cobre os dois casos.
Future<void> _handleShowLastSessionHistory() async {
setState(() => _loadingHistory = true);
TrackingSessionHistory? history;
Object? error;
try {
history = await NemuTracking.instance.getLastSessionHistory();
} catch (e) {
error = e;
}
if (!mounted) return;
setState(() => _loadingHistory = false);
await showDialog<void>(
context: context,
builder: (_) => _LastSessionHistoryDialog(history: history, error: error),
);
}
Future<void> _handleReset() async {
final prefs = await SharedPreferences.getInstance();
final keys = prefs.getKeys().where((k) => k.startsWith('@nemu_sdk:'));
for (final key in keys) {
await prefs.remove(key);
}
setState(() => _status = 'Storage cleared! Reinitializing...');
_initSdk();
}
@override
void dispose() {
_clickIdController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Nemu SDK Example',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Text(
_status,
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed:
_loadingHistory ? null : _handleShowLastSessionHistory,
icon: _loadingHistory
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.history),
label: Text(
_loadingHistory
? 'Loading...'
: 'Show last session history',
),
),
),
const SizedBox(height: 24),
if (kDebugMode) _buildDebugSection(),
],
),
),
),
),
);
}
Widget _buildDebugSection() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Install Referrer Override',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 12),
TextField(
controller: _clickIdController,
decoration: const InputDecoration(
hintText: 'Enter click_id (e.g. uuid)',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
autocorrect: false,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _handleSetClickId,
child: const Text('Set click_id'),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _handleClearClickId,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
foregroundColor: Colors.white,
),
child: const Text('Clear'),
),
],
),
if (_activeClickId != null) ...[
const SizedBox(height: 8),
Text(
'Active: $_activeClickId',
style: const TextStyle(
fontSize: 13,
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
const SizedBox(height: 4),
const Text(
'Set the click_id before resetting the SDK to simulate an install referrer on the next deferred deep link check.',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _handleReset,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Reset SDK (test deferred deep link)'),
),
),
],
),
);
}
}
/// Lista o que `getLastSessionHistory()` devolveu.
///
/// `TrackingSessionHistory` não tem `toJson`, então os campos são enumerados à
/// mão. Os nulos aparecem como `null` em vez de serem omitidos: numa tela de
/// diagnóstico, "o backend não mandou este campo" é justamente a informação
/// que se está procurando.
class _LastSessionHistoryDialog extends StatelessWidget {
const _LastSessionHistoryDialog({this.history, this.error});
final TrackingSessionHistory? history;
final Object? error;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('getLastSessionHistory()'),
content: SizedBox(width: double.maxFinite, child: _buildContent()),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
);
}
Widget _buildContent() {
if (error != null) {
return Text(
'${error.runtimeType}: $error',
style: const TextStyle(fontSize: 13, color: Colors.red),
);
}
final h = history;
if (h == null) {
return const Text(
'Returned null — no session history for this device yet. Track an '
'event (open a deep link) and try again.',
style: TextStyle(fontSize: 13, color: Colors.grey),
);
}
final tracking = h.tracking;
final entries = <(String, Object?)>[
('id', h.id),
('trackingSessionId', h.trackingSessionId),
('trackingId', h.trackingId),
('provider', h.provider),
('referrer', h.referrer),
('origin', h.origin),
('utmSource', h.utmSource),
('utmMedium', h.utmMedium),
('utmCampaign', h.utmCampaign),
('utmCampaignName', h.utmCampaignName),
('utmContent', h.utmContent),
('utmTerm', h.utmTerm),
('utmCampaignId', h.utmCampaignId),
('utmAdsetId', h.utmAdsetId),
('utmAdsetName', h.utmAdsetName),
('utmAdId', h.utmAdId),
('utmAdName', h.utmAdName),
('googleKeyword', h.googleKeyword),
('traySessionId', h.traySessionId),
('vtexSessionId', h.vtexSessionId),
('nuvemShopId', h.nuvemShopId),
('fbp', h.fbp),
('fbc', h.fbc),
('fbclid', h.fbclid),
('fbcUnix', h.fbcUnix),
('fbpUnix', h.fbpUnix),
('clientHash', h.clientHash),
('dashboardId', h.dashboardId),
('accountId', h.accountId),
('createdAt', h.createdAt),
('updatedAt', h.updatedAt),
if (tracking == null)
('tracking', null)
else ...[
('tracking.id', tracking.id),
('tracking.name', tracking.name),
('tracking.origin', tracking.origin),
('tracking.dashboardId', tracking.dashboardId),
('tracking.accountId', tracking.accountId),
('tracking.productId', tracking.productId),
],
];
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final (key, value) in entries) _buildRow(key, value),
],
),
);
}
Widget _buildRow(String key, Object? value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
// Cabe a maior chave (`tracking.dashboardId`) sem quebrar no meio
// do nome; valores longos quebram, e quebrar valor lê melhor que
// quebrar identificador.
width: 158,
child: Text(
key,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
fontFamily: 'monospace',
),
),
),
Expanded(
child: Text(
value?.toString() ?? 'null',
style: TextStyle(
fontSize: 12,
fontFamily: 'monospace',
color: value == null ? Colors.grey : Colors.black87,
),
),
),
],
),
);
}
}