attriax_flutter 0.1.0
attriax_flutter: ^0.1.0 copied to clipboard
Attriax SDK for Flutter - A powerful and easy-to-use solution for mobile attribution, deep-linking, and analytics.
import 'dart:async';
import 'package:attriax_flutter/attriax_flutter.dart';
import 'package:flutter/material.dart';
import 'example_app_configuration.dart';
import 'example_attriax_sdk.dart';
const String exampleDefaultAppToken = 'ax_b62ee57056374b76aa09b26fa071e561';
const String exampleAppToken = String.fromEnvironment(
'ATTRIAX_APP_TOKEN',
defaultValue: exampleDefaultAppToken,
);
const String exampleApiBaseUrl = String.fromEnvironment('ATTRIAX_API_BASE_URL');
bool isExampleAppConfigured({required String appToken}) =>
!appToken.startsWith('ax_your_');
void ensureExampleAppConfigured({required String appToken}) {
if (!isExampleAppConfigured(appToken: appToken)) {
throw StateError(
'Replace the example Attriax app token before running this app.',
);
}
}
Attriax _createExampleAttriax(ExampleAppConfiguration configuration) {
ensureExampleAppConfigured(appToken: configuration.appToken);
final apiBaseUrl = configuration.normalizedApiBaseUrl;
return Attriax(
config: apiBaseUrl == null
? AttriaxConfig(
appToken: configuration.appToken,
sdkMetadata: const <String, Object?>{
'surface': 'package_example',
'purpose': 'docs_and_demo',
},
)
: AttriaxConfig(
appToken: configuration.appToken,
apiBaseUrl: apiBaseUrl,
sdkMetadata: const <String, Object?>{
'surface': 'package_example',
'purpose': 'docs_and_demo',
},
),
);
}
LiveExampleAttriaxSdk _createExampleSdk(
ExampleAppConfiguration configuration,
) => LiveExampleAttriaxSdk(_createExampleAttriax(configuration));
final GlobalKey<NavigatorState> _exampleNavigatorKey =
GlobalKey<NavigatorState>();
class _ExampleRouteDestination {
const _ExampleRouteDestination({
required this.routeName,
required this.title,
required this.description,
});
final String routeName;
final String title;
final String description;
}
class _ExampleDeepLinkPageArgs {
const _ExampleDeepLinkPageArgs({
required this.deepLink,
required this.navigationSource,
required this.title,
required this.description,
});
final AttriaxDeepLink deepLink;
final String navigationSource;
final String title;
final String description;
}
String _normalizedExampleDeepLinkPath(Uri uri) {
final candidate = uri.path.isNotEmpty && uri.path != '/'
? uri.path
: uri.host;
final trimmed = candidate.trim();
if (trimmed.isEmpty) {
return '';
}
return trimmed
.replaceFirst(RegExp(r'^/+'), '')
.replaceFirst(RegExp(r'/+$'), '');
}
AttriaxDeepLink _buildExampleDeepLink(Uri uri, Map<String, String>? data) =>
AttriaxDeepLink(path: _normalizedExampleDeepLinkPath(uri), data: data);
String _describeExampleDeepLink(Uri? uri) {
if (uri == null) {
return 'none';
}
final normalizedPath = _normalizedExampleDeepLinkPath(uri);
return normalizedPath.isEmpty ? uri.toString() : normalizedPath;
}
String _describeExampleResolution(
Uri? uri,
AttriaxDeepLinkResolution? resolution,
) {
if (resolution == null) {
return 'none';
}
if (!resolution.found) {
return 'external or unmatched';
}
return _describeExampleDeepLink(uri);
}
_ExampleRouteDestination _resolveExampleRoute(AttriaxDeepLink deepLink) {
final segments = deepLink.path
.split('/')
.where((segment) => segment.trim().isNotEmpty)
.toList(growable: false);
if (segments.isEmpty) {
return const _ExampleRouteDestination(
routeName: '/deep-link',
title: 'Deep Link Screen',
description: 'Fallback destination for unmatched route patterns.',
);
}
switch (segments.first) {
case 'promo':
return const _ExampleRouteDestination(
routeName: '/promo',
title: 'Promo Screen',
description:
'This simulates opening a campaign or promotion screen from a matched deep link.',
);
case 'profile':
return const _ExampleRouteDestination(
routeName: '/profile',
title: 'Profile Screen',
description:
'This simulates routing into a user-specific area after deep-link matching succeeds.',
);
default:
return const _ExampleRouteDestination(
routeName: '/deep-link',
title: 'Deep Link Screen',
description: 'Fallback destination for unmatched route patterns.',
);
}
}
void _openExampleDeepLink(AttriaxDeepLink deepLink, {required String source}) {
final navigator = _exampleNavigatorKey.currentState;
if (navigator == null) {
return;
}
final destination = _resolveExampleRoute(deepLink);
navigator.pushNamed(
destination.routeName,
arguments: _ExampleDeepLinkPageArgs(
deepLink: deepLink,
navigationSource: source,
title: destination.title,
description: destination.description,
),
);
}
String _maskExampleAppToken(String appToken) {
if (appToken.length <= 8) {
return appToken;
}
return '${appToken.substring(0, 4)}...${appToken.substring(appToken.length - 4)}';
}
Route<void> _onGenerateExampleRoute(
RouteSettings settings, {
required ExampleAttriaxSdk sdk,
ExampleAppConfiguration? configuration,
Future<void> Function()? onResetConfiguration,
}) {
switch (settings.name) {
case '/':
return MaterialPageRoute<void>(
builder: (_) => ExampleHomePage(
sdk: sdk,
configuration: configuration,
onResetConfiguration: onResetConfiguration,
),
settings: settings,
);
case '/promo':
case '/profile':
case '/deep-link':
final args = settings.arguments as _ExampleDeepLinkPageArgs?;
return MaterialPageRoute<void>(
builder: (_) => _ExampleDeepLinkDestinationPage(args: args),
settings: settings,
);
default:
return MaterialPageRoute<void>(
builder: (_) => ExampleHomePage(
sdk: sdk,
configuration: configuration,
onResetConfiguration: onResetConfiguration,
),
settings: settings,
);
}
}
// ── Entry point ───────────────────────────────────────────────────────────────
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final configurationStore = ExampleAppConfigurationStore();
final storedConfiguration = await configurationStore.load();
final initialConfiguration =
storedConfiguration ??
(isExampleAppConfigured(appToken: exampleAppToken)
? ExampleAppConfiguration(
appToken: exampleAppToken,
apiBaseUrl: exampleApiBaseUrl.isEmpty ? null : exampleApiBaseUrl,
)
: null);
ExampleAttriaxSdk? initialSdk;
String? initialBootstrapError;
if (initialConfiguration != null) {
try {
initialSdk = _createExampleSdk(initialConfiguration);
// Recommended: await initialization during startup so the SDK has
// restored persisted state, collected context, and started listeners
// before the UI. If your app must remain non-blocking, you can
// intentionally choose unawaited(attriax.init()) instead.
await initialSdk.init();
} catch (error) {
initialBootstrapError =
'Saved configuration failed to initialize: ${_formatSetupError(error)}';
await initialSdk?.dispose();
initialSdk = null;
}
}
runApp(
AttriaxPackageExampleApp(
sdk: initialSdk,
ownsProvidedSdk: initialSdk != null,
initialConfiguration: initialConfiguration,
configurationStore: configurationStore,
initialBootstrapError: initialBootstrapError,
),
);
}
// ── App widget ────────────────────────────────────────────────────────────────
class AttriaxPackageExampleApp extends StatefulWidget {
const AttriaxPackageExampleApp({
super.key,
this.sdk,
this.ownsProvidedSdk = false,
this.initialConfiguration,
this.configurationStore,
this.initialBootstrapError,
});
final ExampleAttriaxSdk? sdk;
final bool ownsProvidedSdk;
final ExampleAppConfiguration? initialConfiguration;
final ExampleAppConfigurationStore? configurationStore;
final String? initialBootstrapError;
@override
State<AttriaxPackageExampleApp> createState() =>
_AttriaxPackageExampleAppState();
}
class _AttriaxPackageExampleAppState extends State<AttriaxPackageExampleApp> {
ExampleAttriaxSdk? _sdk;
bool _ownsSdk = false;
bool _isInitializing = false;
String? _bootstrapError;
ExampleAppConfiguration? _configuration;
ExampleAppConfigurationStore get _configurationStore =>
widget.configurationStore ?? ExampleAppConfigurationStore();
@override
void initState() {
super.initState();
_sdk = widget.sdk;
_ownsSdk = widget.ownsProvidedSdk;
_bootstrapError = widget.initialBootstrapError;
_configuration = widget.initialConfiguration;
}
@override
void dispose() {
if (_ownsSdk) {
unawaited(_sdk?.dispose() ?? Future<void>.value());
}
super.dispose();
}
Future<void> _applyConfiguration(
ExampleAppConfiguration configuration,
) async {
setState(() {
_isInitializing = true;
_bootstrapError = null;
});
LiveExampleAttriaxSdk? nextSdk;
try {
nextSdk = _createExampleSdk(configuration);
await nextSdk.init();
await _configurationStore.save(configuration);
final previousSdk = _ownsSdk ? _sdk : null;
if (!mounted) {
await nextSdk.dispose();
return;
}
setState(() {
_sdk = nextSdk;
_ownsSdk = true;
_configuration = configuration;
_isInitializing = false;
});
await previousSdk?.dispose();
} catch (error) {
await nextSdk?.dispose();
if (!mounted) {
return;
}
setState(() {
_bootstrapError = 'Initialization failed: ${_formatSetupError(error)}';
_isInitializing = false;
});
}
}
Future<void> _resetConfiguration() async {
final previousSdk = _ownsSdk ? _sdk : null;
_exampleNavigatorKey.currentState?.popUntil((route) => route.isFirst);
await _configurationStore.clear();
if (!mounted) {
await previousSdk?.dispose();
return;
}
setState(() {
_sdk = null;
_ownsSdk = false;
_configuration = null;
_bootstrapError = null;
_isInitializing = false;
});
await previousSdk?.dispose();
}
@override
Widget build(BuildContext context) {
final sdk = _sdk;
if (sdk == null) {
return MaterialApp(
title: 'Attriax Example',
theme: ThemeData(
colorSchemeSeed: const Color(0xFF0F766E),
useMaterial3: true,
),
home: ExampleSetupPage(
initialConfiguration: _configuration,
isInitializing: _isInitializing,
bootstrapError: _bootstrapError,
onApplyConfiguration: _applyConfiguration,
),
);
}
return MaterialApp(
title: 'Attriax Example',
navigatorKey: _exampleNavigatorKey,
navigatorObservers: sdk.buildNavigatorObservers(),
theme: ThemeData(
colorSchemeSeed: const Color(0xFF0F766E),
useMaterial3: true,
),
onGenerateRoute: (settings) => _onGenerateExampleRoute(
settings,
sdk: sdk,
configuration: _configuration,
onResetConfiguration: _ownsSdk ? _resetConfiguration : null,
),
initialRoute: '/',
);
}
}
class ExampleSetupPage extends StatefulWidget {
const ExampleSetupPage({
super.key,
required this.onApplyConfiguration,
this.initialConfiguration,
this.isInitializing = false,
this.bootstrapError,
});
final Future<void> Function(ExampleAppConfiguration configuration)
onApplyConfiguration;
final ExampleAppConfiguration? initialConfiguration;
final bool isInitializing;
final String? bootstrapError;
@override
State<ExampleSetupPage> createState() => _ExampleSetupPageState();
}
class _ExampleSetupPageState extends State<ExampleSetupPage> {
late final TextEditingController _appTokenController = TextEditingController(
text: widget.initialConfiguration?.appToken ?? exampleAppToken,
);
late final TextEditingController _apiBaseUrlController =
TextEditingController(
text: widget.initialConfiguration?.displayApiBaseUrl ?? '',
);
String? _validationError;
@override
void dispose() {
_appTokenController.dispose();
_apiBaseUrlController.dispose();
super.dispose();
}
Future<void> _submit() async {
final appToken = _appTokenController.text.trim();
try {
ensureExampleAppConfigured(appToken: appToken);
final configuration = ExampleAppConfiguration(
appToken: appToken,
apiBaseUrl: normalizeExampleApiBaseUrl(_apiBaseUrlController.text),
);
setState(() {
_validationError = null;
});
await widget.onApplyConfiguration(configuration);
} catch (error) {
setState(() {
_validationError = _formatSetupError(error);
});
return;
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final errorText = _validationError ?? widget.bootstrapError;
return Scaffold(
appBar: AppBar(title: const Text('Attriax Example Setup')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Configure the public example',
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 12),
const Text(
'This example no longer requires dart-defines or source edits. '
'Enter a real Attriax app token on first launch and the app '
'will store it locally on this device for later runs.',
),
const SizedBox(height: 16),
TextField(
controller: _appTokenController,
enabled: !widget.isInitializing,
decoration: const InputDecoration(
labelText: 'Attriax app token',
helperText:
'Required. Replace the placeholder token before initializing.',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _apiBaseUrlController,
enabled: !widget.isInitializing,
decoration: const InputDecoration(
labelText: 'API base URL',
helperText:
'Optional. Leave blank to use the production default.',
border: OutlineInputBorder(),
),
),
if (errorText != null) ...<Widget>[
const SizedBox(height: 12),
Text(
errorText,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.error,
),
),
],
const SizedBox(height: 16),
FilledButton(
onPressed: widget.isInitializing ? null : _submit,
child: Text(
widget.isInitializing
? 'Initializing SDK...'
: 'Save configuration and initialize',
),
),
],
),
),
),
),
),
);
}
}
String _formatSetupError(Object error) {
if (error is ArgumentError) {
return error.message?.toString() ?? error.toString();
}
final description = error.toString();
const badStatePrefix = 'Bad state: ';
if (description.startsWith(badStatePrefix)) {
return description.substring(badStatePrefix.length);
}
return description;
}
class ExampleHomePage extends StatefulWidget {
const ExampleHomePage({
super.key,
required this.sdk,
this.configuration,
this.onResetConfiguration,
});
final ExampleAttriaxSdk sdk;
final ExampleAppConfiguration? configuration;
final Future<void> Function()? onResetConfiguration;
@override
State<ExampleHomePage> createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage> {
final TextEditingController _manualPathController = TextEditingController(
text: 'promo/spring-launch',
);
final TextEditingController _firebaseTokenController = TextEditingController(
text: 'demo_firebase_registration_token',
);
final TextEditingController _apnsTokenController = TextEditingController(
text: 'demo_apns_device_token',
);
StreamSubscription<AttriaxDeepLinkEvent>? _deepLinkSubscription;
StreamSubscription<AttriaxSynchronizationState>? _synchronizationSubscription;
bool _sdkEnabled = true;
bool _eventsEnabled = true;
String _status = 'SDK initialized.';
String _firebaseTokenStatus = 'Not sent to Attriax yet.';
String _apnsTokenStatus = 'Not sent to Attriax yet.';
AttriaxInstallReferrerDetails? _startupInstallReferrer;
AttriaxDeepLinkEvent? _startupInitialDeepLink;
AttriaxDynamicLinkRecord? _lastCreatedDynamicLink;
Uri? _lastDeepLinkUri;
AttriaxDeepLinkResolution? _lastResolution;
@override
void initState() {
super.initState();
_deepLinkSubscription = widget.sdk.deepLinks.stream.listen(
_handleDeepLinkEvent,
);
_synchronizationSubscription = widget.sdk.synchronizationStates.listen((_) {
if (!mounted) {
return;
}
setState(() {});
});
if (widget.sdk.isInitialized) {
_syncStateFromSdk();
}
}
@override
void dispose() {
unawaited(_deepLinkSubscription?.cancel() ?? Future<void>.value());
unawaited(_synchronizationSubscription?.cancel() ?? Future<void>.value());
_manualPathController.dispose();
_firebaseTokenController.dispose();
_apnsTokenController.dispose();
super.dispose();
}
void _syncStateFromSdk() {
setState(() {
_sdkEnabled = widget.sdk.enabled;
_eventsEnabled = widget.sdk.eventsEnabled;
_status = widget.sdk.enabled
? 'Initialized. Startup attribution is available via referrer and deepLinks.'
: 'Initialized in disabled mode.';
});
}
Future<void> _handleDeepLinkEvent(AttriaxDeepLinkEvent event) async {
if (mounted) {
setState(() {
_lastDeepLinkUri = event.uri;
_status = 'Received deep link: ${_describeExampleDeepLink(event.uri)}';
});
}
try {
final resolution = await event.resolve();
if (!mounted) {
return;
}
setState(() {
_lastResolution = resolution;
_status = resolution.found
? 'Matched deep link: ${_describeExampleDeepLink(event.uri)}'
: 'Recorded external deep link: ${_describeExampleDeepLink(event.uri)}';
});
if (resolution.found) {
_openExampleDeepLink(
_buildExampleDeepLink(event.uri, resolution.data),
source: event.isDeferred ? 'deferred_app_open' : 'matched_conversion',
);
}
} catch (error) {
if (!mounted) {
return;
}
setState(() {
_status = 'Deep link processing failed: $error';
});
}
}
Future<void> _loadStartupAttribution() async {
setState(() => _status = 'Loading startup attribution...');
try {
final initialDeepLink = await widget.sdk.deepLinks
.waitForInitialDeepLink();
final initialResolution = initialDeepLink == null
? null
: await initialDeepLink.resolve();
final installReferrer = await widget.sdk.getOriginalInstallReferrer();
if (!mounted) return;
setState(() {
_startupInstallReferrer = installReferrer;
_startupInitialDeepLink = initialDeepLink;
_lastDeepLinkUri = initialDeepLink?.uri ?? _lastDeepLinkUri;
_lastResolution = initialResolution ?? _lastResolution;
_status = installReferrer == null && initialDeepLink == null
? 'Startup attribution loaded. No original install referrer or initial deep link found.'
: 'Startup attribution loaded.';
});
} catch (error) {
if (!mounted) return;
setState(() => _status = 'Startup attribution failed: $error');
}
}
Future<void> _recordSampleEvent() async {
await widget.sdk.recordEvent(
'purchase_completed',
eventData: const <String, Object?>{
'value': 99,
'currency': 'USD',
'plan': 'pro',
},
);
if (!mounted) return;
setState(() => _status = 'Queued purchase_completed event.');
}
Future<void> _registerFirebaseToken() async {
final token = _firebaseTokenController.text.trim();
await widget.sdk.registerFirebaseMessagingToken(
token.isEmpty ? null : token,
metadata: const <String, Object?>{
'surface': 'package_example',
'source': 'manual_demo',
},
);
if (!mounted) {
return;
}
setState(() {
_firebaseTokenStatus = token.isEmpty
? 'Firebase token cleared in Attriax.'
: 'Firebase token sent to Attriax.';
_status = _firebaseTokenStatus;
});
}
Future<void> _clearFirebaseToken() async {
_firebaseTokenController.clear();
await _registerFirebaseToken();
}
Future<void> _registerApnsToken() async {
final token = _apnsTokenController.text.trim();
await widget.sdk.registerApplePushToken(
token.isEmpty ? null : token,
metadata: const <String, Object?>{
'surface': 'package_example',
'source': 'manual_demo',
},
);
if (!mounted) {
return;
}
setState(() {
_apnsTokenStatus = token.isEmpty
? 'APNs token cleared in Attriax.'
: 'APNs token sent to Attriax.';
_status = _apnsTokenStatus;
});
}
Future<void> _clearApnsToken() async {
_apnsTokenController.clear();
await _registerApnsToken();
}
Future<void> _setSampleUser() async {
await widget.sdk.setUser('demo-user-123', userName: 'Package Example User');
if (!mounted) return;
setState(() => _status = 'Queued setUser for demo-user-123.');
}
Future<void> _createSampleDynamicLink() async {
final result = await widget.sdk.createDynamicLink(
name: 'Package example dynamic link',
destinationUrl: 'https://attriax.com/invite',
group: 'package-example',
socialPreview: const AttriaxDynamicLinkSocialPreview(
title: 'Open the Attriax example app',
description:
'Example-generated dynamic link with an attached campaign payload.',
),
data: const <String, Object?>{
'source': 'flutter_package_example',
'campaign': 'dynamic-link-demo',
},
);
if (!mounted) return;
setState(() {
_lastCreatedDynamicLink = result.link;
_status = 'Created dynamic link: ${result.link.shortUrl}';
});
}
Future<void> _recordManualDeepLink() async {
final normalizedPath = _manualPathController.text.trim();
final manualUri = Uri(
path: normalizedPath.startsWith('/')
? normalizedPath
: '/$normalizedPath',
);
final resolution = await widget.sdk.recordDeepLink(
linkPath: normalizedPath,
source: 'package_example_manual',
metadata: const <String, Object?>{'acceptedBy': 'example_button'},
);
if (!mounted) return;
setState(() {
_lastDeepLinkUri = manualUri;
_lastResolution = resolution ?? _lastResolution;
_status = resolution == null
? 'Manual deep-link report sent.'
: resolution.found
? 'Manual deep-link resolution matched ${_describeExampleDeepLink(manualUri)}.'
: 'Manual deep-link recorded as external or unmatched.';
});
if (resolution != null && resolution.found) {
_openExampleDeepLink(
_buildExampleDeepLink(manualUri, resolution.data),
source: 'manual_report',
);
}
}
void _toggleSdk(bool value) {
setState(() => _sdkEnabled = value);
widget.sdk.enabled = value;
setState(() => _status = 'SDK enabled set to $value.');
}
void _toggleEvents(bool value) {
setState(() => _eventsEnabled = value);
widget.sdk.eventsEnabled = value;
setState(() => _status = 'Custom event sending set to $value.');
}
void _previewNavigation(String path) {
_openExampleDeepLink(
AttriaxDeepLink(
path: path,
data: <String, Object?>{'preview': true, 'path': path},
),
source: 'local_preview',
);
setState(() {
_status = 'Previewed app navigation for $path.';
});
}
String _synchronizationLabel(AttriaxSynchronizationState state) {
switch (state) {
case AttriaxSynchronizationState.initializing:
return 'Initializing';
case AttriaxSynchronizationState.synchronizing:
return 'Synchronizing';
case AttriaxSynchronizationState.deferred:
return 'Deferred';
case AttriaxSynchronizationState.synchronized:
return 'Synchronized';
case AttriaxSynchronizationState.offline:
return 'Offline';
case AttriaxSynchronizationState.failed:
return 'Failed';
case AttriaxSynchronizationState.disabled:
return 'Disabled';
}
}
Color _synchronizationColor(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
switch (widget.sdk.synchronizationState) {
case AttriaxSynchronizationState.synchronized:
return scheme.primaryContainer;
case AttriaxSynchronizationState.initializing:
case AttriaxSynchronizationState.synchronizing:
case AttriaxSynchronizationState.deferred:
return scheme.secondaryContainer;
case AttriaxSynchronizationState.offline:
case AttriaxSynchronizationState.failed:
return scheme.errorContainer;
case AttriaxSynchronizationState.disabled:
return scheme.surfaceContainerHighest;
}
}
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final synchronizationState = widget.sdk.synchronizationState;
final configuration = widget.configuration;
return Scaffold(
appBar: AppBar(
title: const Text('Attriax Example'),
actions: <Widget>[
if (widget.onResetConfiguration != null)
IconButton(
onPressed: widget.onResetConfiguration,
tooltip: 'Reset saved example configuration',
icon: const Icon(Icons.tune),
),
],
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Minimal Integration Example',
style: textTheme.titleMedium,
),
const SizedBox(height: 12),
const Text(
'This is the public example shipped with the package. '
'It demonstrates the recommended awaited init flow, '
'SDK synchronization state, deep-link streams, and '
'how to navigate to real app screens when a link is matched.',
),
const SizedBox(height: 8),
Text(
'Use the setup screen to save a real app token on '
'first launch. The example stores it locally so you '
'do not need dart-defines or source edits for each run.',
style: textTheme.bodySmall,
),
],
),
),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('SDK Status', style: textTheme.titleMedium),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: _synchronizationColor(context),
borderRadius: BorderRadius.circular(999),
),
child: Text(
'Synchronization: ${_synchronizationLabel(synchronizationState)}',
style: textTheme.labelLarge,
),
),
const SizedBox(height: 12),
Text(_status),
const SizedBox(height: 8),
Text('Initialized: ${widget.sdk.isInitialized}'),
Text('Synchronized: ${widget.sdk.isSynchronized}'),
Text('SDK enabled: ${widget.sdk.enabled}'),
Text('Events enabled: ${widget.sdk.eventsEnabled}'),
Text('First launch: ${widget.sdk.isFirstLaunch}'),
Text('Device ID: ${widget.sdk.deviceId ?? 'pending…'}'),
Text(
'Configured app token: ${configuration == null ? 'test or injected SDK' : _maskExampleAppToken(configuration.appToken)}',
),
Text(
'API base URL: ${configuration?.displayApiBaseUrl ?? 'production default'}',
),
const SizedBox(height: 8),
Text(
'Install referrer campaign: ${_startupInstallReferrer?.campaign ?? 'none'}',
),
Text(
'Initial deep link: ${_describeExampleDeepLink(_startupInitialDeepLink?.uri)}',
),
],
),
),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Router Demo', style: textTheme.titleMedium),
const SizedBox(height: 12),
const Text(
'In real usage, the deepLinks stream decides which screen '
'to open. These buttons call the same navigation helper '
'used by the SDK listener so you can preview the flow '
'without waiting for a backend match.',
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: <Widget>[
FilledButton.tonal(
onPressed: () =>
_previewNavigation('promo/spring-launch'),
child: const Text('Preview promo route'),
),
FilledButton.tonal(
onPressed: () =>
_previewNavigation('profile/demo-user-123'),
child: const Text('Preview profile route'),
),
FilledButton.tonal(
onPressed: () =>
_previewNavigation('custom/anything-else'),
child: const Text('Preview fallback route'),
),
],
),
],
),
),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Controls', style: textTheme.titleMedium),
const SizedBox(height: 12),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: const Text('SDK enabled'),
value: _sdkEnabled,
onChanged: _toggleSdk,
),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: const Text('Custom event sending enabled'),
value: _eventsEnabled,
onChanged: _toggleEvents,
),
const SizedBox(height: 8),
OutlinedButton(
onPressed: widget.sdk.isInitialized
? _loadStartupAttribution
: null,
child: const Text('Load startup attribution result'),
),
],
),
),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Actions', style: textTheme.titleMedium),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: widget.sdk.isInitialized
? _recordSampleEvent
: null,
child: const Text('Queue purchase_completed event'),
),
const SizedBox(height: 8),
FilledButton.tonal(
onPressed: widget.sdk.isInitialized
? _setSampleUser
: null,
child: const Text('Queue setUser (demo-user-123)'),
),
const SizedBox(height: 8),
FilledButton.tonal(
onPressed: widget.sdk.isInitialized
? _createSampleDynamicLink
: null,
child: const Text('Create sample dynamic link'),
),
const SizedBox(height: 16),
TextField(
controller: _manualPathController,
decoration: const InputDecoration(
labelText: 'Manual deep link path',
helperText:
'Use when another router handles the incoming link.',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
FilledButton.tonal(
onPressed: widget.sdk.isInitialized
? _recordManualDeepLink
: null,
child: const Text('Report manual deep link'),
),
],
),
),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Uninstall Token Registration',
style: textTheme.titleMedium,
),
const SizedBox(height: 12),
const Text(
'Real host apps usually call these methods after '
'Firebase Messaging or APNs gives them a token. This '
'example lets you manually send a value to Attriax so '
'you can verify the integration flow without adding '
'Firebase to the demo app.',
),
const SizedBox(height: 8),
Text(
'Android normally sends only the Firebase token. Apple '
'platforms can send the Firebase token, the APNs token, '
'or both when your app has access to both values.',
style: textTheme.bodySmall,
),
const SizedBox(height: 16),
TextField(
controller: _firebaseTokenController,
decoration: const InputDecoration(
labelText: 'Firebase registration token',
helperText:
'Example host call: attriax.registerFirebaseMessagingToken(token)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: <Widget>[
FilledButton.tonal(
onPressed: widget.sdk.isInitialized
? _registerFirebaseToken
: null,
child: const Text('Send Firebase token'),
),
OutlinedButton(
onPressed: widget.sdk.isInitialized
? _clearFirebaseToken
: null,
child: const Text('Clear Firebase token'),
),
],
),
const SizedBox(height: 8),
Text('Firebase status: $_firebaseTokenStatus'),
const SizedBox(height: 16),
TextField(
controller: _apnsTokenController,
decoration: const InputDecoration(
labelText: 'Apple APNs device token',
helperText:
'Example host call: attriax.registerApplePushToken(token)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: <Widget>[
FilledButton.tonal(
onPressed: widget.sdk.isInitialized
? _registerApnsToken
: null,
child: const Text('Send APNs token'),
),
OutlinedButton(
onPressed: widget.sdk.isInitialized
? _clearApnsToken
: null,
child: const Text('Clear APNs token'),
),
],
),
const SizedBox(height: 8),
Text('APNs status: $_apnsTokenStatus'),
],
),
),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Latest Deep Link State',
style: textTheme.titleMedium,
),
const SizedBox(height: 12),
Text(
'Last deep link: ${_describeExampleDeepLink(_lastDeepLinkUri)}',
),
Text(
'Last resolution: ${_describeExampleResolution(_lastDeepLinkUri, _lastResolution)}',
),
Text(
'Last created short URL: ${_lastCreatedDynamicLink?.shortUrl ?? 'none'}',
),
Text(
'Last resolution found: ${_lastResolution?.found.toString() ?? 'none'}',
),
],
),
),
),
],
),
),
),
);
}
}
class _ExampleDeepLinkDestinationPage extends StatelessWidget {
const _ExampleDeepLinkDestinationPage({required this.args});
final _ExampleDeepLinkPageArgs? args;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final deepLink = args?.deepLink;
final metadata = deepLink?.data ?? const <String, Object?>{};
return Scaffold(
appBar: AppBar(title: Text(args?.title ?? 'Deep Link Screen')),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
args?.title ?? 'Deep Link Screen',
style: theme.textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(args?.description ?? 'No deep-link payload available.'),
const SizedBox(height: 16),
Text('Matched path: ${deepLink?.path ?? 'none'}'),
Text(
'Navigation source: ${args?.navigationSource ?? 'unknown'}',
),
const SizedBox(height: 16),
Text('Metadata', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
if (metadata.isEmpty)
const Text('No metadata was attached to this deep link.')
else
for (final entry in metadata.entries)
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text('${entry.key}: ${entry.value}'),
),
const Spacer(),
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Back to example home'),
),
],
),
),
),
),
),
);
}
}