dub_flutter 0.0.2
dub_flutter: ^0.0.2 copied to clipboard
An unofficial Flutter SDK for Dub - track clicks, leads, and sales with the open-source link management platform.
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:dub_flutter/dub_flutter.dart';
import 'package:flutter/material.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const DubExampleApp());
}
class DubExampleApp extends StatelessWidget {
const DubExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Dub Flutter Example',
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
home: const DubDebugHome(),
);
}
}
class DubDebugHome extends StatefulWidget {
const DubDebugHome({super.key});
@override
State<DubDebugHome> createState() => _DubDebugHomeState();
}
class _DubDebugHomeState extends State<DubDebugHome> {
final _publishableKeyController = TextEditingController(text: 'dub_xxxxx');
final _domainController = TextEditingController(text: 'dub.sh');
final _customerExternalIdController = TextEditingController(text: 'user_123');
final _leadEventNameController = TextEditingController(text: 'Sign up');
final _saleEventNameController = TextEditingController(text: 'subscription');
final _leadEventNameForSaleController = TextEditingController(
text: 'Sign up',
);
final _amountController = TextEditingController(text: '9.99');
final _currencyController = TextEditingController(text: 'usd');
PaymentProcessor _paymentProcessor = PaymentProcessor.custom;
final _appLinks = AppLinks();
StreamSubscription<Uri>? _sub;
bool _initialized = false;
Uri? _lastIncomingUri;
TrackOpenResponse? _lastOpen;
TrackLeadResponse? _lastLead;
TrackSaleResponse? _lastSale;
String? _lastError;
@override
void initState() {
super.initState();
_startListeningForLinks();
}
Future<void> _startListeningForLinks() async {
try {
final initial = await _appLinks.getInitialLink();
if (initial != null) {
setState(() {
_lastIncomingUri = initial;
});
}
_sub = _appLinks.uriLinkStream.listen(
(uri) {
setState(() {
_lastIncomingUri = uri;
});
},
onError: (Object error) {
setState(() {
_lastError = 'Deep link stream error: $error';
});
},
);
} catch (e) {
setState(() {
_lastError = 'Failed to initialize deep links: $e';
});
}
}
@override
void dispose() {
_sub?.cancel();
_publishableKeyController.dispose();
_domainController.dispose();
_customerExternalIdController.dispose();
_leadEventNameController.dispose();
_saleEventNameController.dispose();
_leadEventNameForSaleController.dispose();
_amountController.dispose();
_currencyController.dispose();
super.dispose();
}
Future<void> _initSdk() async {
setState(() {
_lastError = null;
});
try {
await Dub.init(
publishableKey: _publishableKeyController.text.trim(),
domain: _domainController.text.trim(),
);
setState(() {
_initialized = true;
});
} catch (e) {
setState(() {
_lastError = 'Dub.init failed: $e';
});
}
}
Future<void> _trackOpen({String? deepLink}) async {
setState(() {
_lastError = null;
_lastOpen = null;
});
try {
final res = await Dub.instance.trackOpen(deepLink: deepLink);
setState(() {
_lastOpen = res;
});
} catch (e) {
setState(() {
_lastError = 'trackOpen failed: $e';
});
}
}
Future<void> _trackLead() async {
setState(() {
_lastError = null;
_lastLead = null;
});
try {
final res = await Dub.instance.trackLead(
eventName: _leadEventNameController.text.trim(),
customerExternalId: _customerExternalIdController.text.trim(),
metadata: const {'example': 'true'},
);
setState(() {
_lastLead = res;
});
} catch (e) {
setState(() {
_lastError = 'trackLead failed: $e';
});
}
}
Future<void> _trackSale() async {
setState(() {
_lastError = null;
_lastSale = null;
});
final amount = double.tryParse(_amountController.text.trim());
if (amount == null) {
setState(() {
_lastError = 'Invalid amount: ${_amountController.text}';
});
return;
}
try {
final res = await Dub.instance.trackSale(
customerExternalId: _customerExternalIdController.text.trim(),
amount: amount,
paymentProcessor: _paymentProcessor,
eventName: _saleEventNameController.text.trim(),
currency: _currencyController.text.trim(),
leadEventName: _leadEventNameForSaleController.text.trim().isEmpty
? null
: _leadEventNameForSaleController.text.trim(),
metadata: const {'example': 'true'},
);
setState(() {
_lastSale = res;
});
} catch (e) {
setState(() {
_lastError = 'trackSale failed: $e';
});
}
}
@override
Widget build(BuildContext context) {
final incoming = _lastIncomingUri?.toString();
return Scaffold(
appBar: AppBar(title: const Text('Dub Deep Link Debugger')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_Section(
title: '1) Initialize SDK',
child: Column(
children: [
TextField(
controller: _publishableKeyController,
decoration: const InputDecoration(
labelText: 'Publishable key',
hintText: 'dub_…',
),
),
const SizedBox(height: 12),
TextField(
controller: _domainController,
decoration: const InputDecoration(
labelText: 'Dub domain',
hintText: 'dub.sh',
),
),
const SizedBox(height: 12),
FilledButton(
onPressed: _initSdk,
child: Text(_initialized ? 'Re-initialize' : 'Initialize'),
),
],
),
),
const SizedBox(height: 16),
_Section(
title: '2) Deep link reception',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Last incoming URI: ${incoming ?? '(none yet)'}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: (!_initialized || _lastIncomingUri == null)
? null
: () => _trackOpen(deepLink: incoming),
child: const Text('Track Open from last incoming URI'),
),
const SizedBox(height: 8),
FilledButton.tonal(
onPressed: _initialized ? () => _trackOpen() : null,
child: const Text(
'Track Open (no deepLink; clipboard/referrer)',
),
),
],
),
),
const SizedBox(height: 16),
_Section(
title: '3) Lead / Sale',
child: Column(
children: [
TextField(
controller: _customerExternalIdController,
decoration: const InputDecoration(
labelText: 'customerExternalId',
),
),
const SizedBox(height: 12),
TextField(
controller: _leadEventNameController,
decoration: const InputDecoration(
labelText: 'Lead eventName',
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: FilledButton(
onPressed: _initialized ? _trackLead : null,
child: const Text('Track Lead'),
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: _saleEventNameController,
decoration: const InputDecoration(
labelText: 'Sale eventName',
),
),
const SizedBox(height: 12),
TextField(
controller: _leadEventNameForSaleController,
decoration: const InputDecoration(
labelText: 'leadEventName (optional)',
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'amount'),
),
),
const SizedBox(width: 12),
SizedBox(
width: 100,
child: TextField(
controller: _currencyController,
decoration: const InputDecoration(
labelText: 'currency',
),
),
),
],
),
const SizedBox(height: 12),
DropdownButtonFormField<PaymentProcessor>(
value: _paymentProcessor,
decoration: const InputDecoration(
labelText: 'paymentProcessor',
),
items: PaymentProcessor.values
.map(
(p) => DropdownMenuItem(value: p, child: Text(p.name)),
)
.toList(),
onChanged: (v) {
if (v == null) return;
setState(() => _paymentProcessor = v);
},
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: FilledButton(
onPressed: _initialized ? _trackSale : null,
child: const Text('Track Sale'),
),
),
],
),
],
),
),
const SizedBox(height: 16),
_Section(
title: '4) Results',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_lastError != null)
Text(
_lastError!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
Text('trackOpen.clickId: ${_lastOpen?.clickId ?? '(none)'}'),
Text('trackOpen.link.key: ${_lastOpen?.link?.key ?? '(none)'}'),
Text('trackLead.click.id: ${_lastLead?.click.id ?? '(none)'}'),
Text(
'trackSale.eventName: ${_lastSale?.eventName ?? '(none)'}',
),
const SizedBox(height: 8),
FutureBuilder<String?>(
future: _initialized
? Dub.instance.clickId
: Future.value(null),
builder: (context, snapshot) {
final clickId = snapshot.data;
return Text('Stored clickId: ${clickId ?? '(none)'}');
},
),
],
),
),
],
),
);
}
}
class _Section extends StatelessWidget {
final String title;
final Widget child;
const _Section({required this.title, required this.child});
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
child,
],
),
),
);
}
}