blux_flutter 0.1.2
blux_flutter: ^0.1.2 copied to clipboard
BluxClient Flutter SDK
example/lib/main.dart
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:blux_flutter/blux_flutter.dart';
import 'package:blux_flutter/blux_flutter_api_stage.dart';
import 'package:blux_flutter/blux_flutter_events/blux_flutter_events.dart';
import 'package:blux_flutter_example/webview/blux_inapp_webview.dart';
import 'package:blux_flutter_example/webview/blux_webview.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final plugin = BluxFlutter();
await plugin.setAPIStage(APIStage.stg);
await plugin.initialize(
bluxApplicationId: '69327603beb1da48e4278eca',
bluxAPIKey: 'EPaHV6oaJZmbwpvY_i6EmHrw8Sq5KhE0iaOZ7ICE',
requestPermissionsOnLaunch: true,
);
runApp(MyApp(plugin: plugin));
}
class MyApp extends StatefulWidget {
final BluxFlutter plugin;
const MyApp({super.key, required this.plugin});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
BluxFlutter get _plugin => widget.plugin;
String _platformVersion = 'Unknown';
late final AppLinks _appLinks;
StreamSubscription<Uri>? _sub;
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
@override
void initState() {
super.initState();
_initPlatformState();
_initDeepLinks();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> _initPlatformState() async {
String version;
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
version =
await _plugin.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
version = 'Failed to get platform version.';
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = version;
});
}
Future<void> _initDeepLinks() async {
_appLinks = AppLinks();
final initialUri = await _appLinks.getInitialLink();
if (initialUri != null) {
_handleDeepLink(initialUri);
}
_sub = _appLinks.uriLinkStream.listen(
(uri) => _handleDeepLink(uri),
onError: (e) {
debugPrint('Deep link error: $e');
},
);
}
void _handleDeepLink(Uri uri) {
// blux://open/https/fmkorea.com/path1/path2?param1=value1¶m2=value2
if (uri.scheme != 'blux') return;
if (uri.host != 'open') return;
final segments = uri.pathSegments; // [https, fmkorea.com, path1, path2]
if (segments.length < 2) return;
final scheme = segments[0]; // https
final host = segments[1]; // fmkorea.com
final path = segments.length > 2
? '/${segments.sublist(2).join('/')}'
: ''; // /path1/path2
final navUri = Uri(
scheme: scheme,
host: host,
path: path,
queryParameters: uri.queryParameters.isEmpty ? null : uri.queryParameters,
);
final nav = _navigatorKey.currentState;
if (nav == null) return;
nav.push(
MaterialPageRoute(
builder: (_) => BluxInAppWebView(initialUrl: navUri.toString()),
),
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _navigatorKey,
home: Scaffold(
appBar: AppBar(title: const Text('Blux SDK for Flutter')),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Running on $_platformVersion\n'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
_plugin.setAPIStage(APIStage.prod);
},
child: const Text('Production'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
_plugin.setAPIStage(APIStage.stg);
},
child: const Text('Staging'),
),
],
),
const SizedBox(height: 16),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
_plugin.initialize(
bluxApplicationId: '69327603beb1da48e4278eca',
bluxAPIKey:
'EPaHV6oaJZmbwpvY_i6EmHrw8Sq5KhE0iaOZ7ICE',
requestPermissionsOnLaunch: true,
);
},
child: const Text('Initialize'),
),
ElevatedButton(
onPressed: _showSignInDialog,
child: const Text('Sign In'),
),
ElevatedButton(
onPressed: () {
_plugin.signOut();
},
child: const Text('Sign Out'),
),
],
),
ElevatedButton(
onPressed: _showSetUserPropertiesDialog,
child: const Text('Set UserProperties'),
),
ElevatedButton(
onPressed: _showSetCustomUserPropertiesDialog,
child: const Text('Set CustomUserProperties'),
),
const SizedBox(height: 16),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
_plugin.sendEvent(
AddProductDetailViewEvent(
itemId: '1234',
customEventProperties: {
'custom_event_properties':
'custom_event_properties',
},
),
);
},
child: const Text('Send AddProductDetailView Event'),
),
ElevatedButton(
onPressed: () {
_plugin.sendEvent(
AddCartaddEvent(
itemId: '1234',
customEventProperties: {
'custom_event_properties':
'custom_event_properties',
},
),
);
},
child: const Text('Send Cartadd Event'),
),
ElevatedButton(
onPressed: () {
_plugin.sendEvent(
AddCustomEvent(
eventType: 'custom_conversion_event',
eventProperties: {
'order_id': 'ORDER_ID_123',
'order_amount': 100,
'paid_amount': 1500,
'items': [
{'id': '1234', 'price': 200, 'quantity': 6},
{'id': '5678', 'price': 300, 'quantity': 1},
],
},
customEventProperties: {
'custom_event_properties':
'custom_event_properties',
},
),
);
},
child: const Text('Send AddCustom Event'),
),
ElevatedButton(
onPressed: () {
_plugin.sendEvent(
AddOrderEvent(
orderAmount: 200,
paidAmount: 100,
items: [
{'id': '1234', 'price': 200, 'quantity': 6},
],
orderId: 'wowowo',
customEventProperties: {
'custom_event_properties':
'custom_event_properties',
},
),
);
},
child: const Text('Send AddPurchase Event'),
),
ElevatedButton(
onPressed: () {
_plugin.sendEvent(
AddOrderEvent(
orderAmount: 200,
paidAmount: 100,
items: [
{'id': '1234', 'price': 200, 'quantity': 6},
{'id': '5678', 'price': 300, 'quantity': 1},
],
orderId: 'num1',
),
);
},
child: const Text('Send AddOrderEvent Event Multiple'),
),
],
),
const SizedBox(height: 16),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Builder(
builder: (innerContext) => ElevatedButton(
onPressed: () {
Navigator.push(
innerContext,
MaterialPageRoute(
builder: (_) => const BluxWebView(
initialUrl:
'https://stg-flutter.sdk-demo.blux.ai/?application_id=69327603beb1da48e4278eca&api_key=EPaHV6oaJZmbwpvY_i6EmHrw8Sq5KhE0iaOZ7ICE',
),
),
);
},
child: const Text('Open WebView (webview_flutter)'),
),
),
Builder(
builder: (innerContext) => ElevatedButton(
onPressed: () {
Navigator.push(
innerContext,
MaterialPageRoute(
builder: (_) => const BluxInAppWebView(
initialUrl:
'https://stg-flutter.sdk-demo.blux.ai/?application_id=69327603beb1da48e4278eca&api_key=EPaHV6oaJZmbwpvY_i6EmHrw8Sq5KhE0iaOZ7ICE',
),
),
);
},
child: const Text(
'Open WebView (flutter_inappwebview)',
),
),
),
],
),
],
),
),
),
),
);
}
void _showSignInDialog() {
final dialogContext = _navigatorKey.currentState!.overlay!.context;
showDialog<String>(
context: dialogContext,
builder: (ctx) {
String v = '';
bool okEnabled() => v.trim().isNotEmpty;
return StatefulBuilder(
builder: (ctx, setState) => AlertDialog(
title: const Text('Sign In'),
content: TextField(
autofocus: true,
onChanged: (s) => setState(() => v = s),
onSubmitted: (_) {
if (okEnabled()) {
Navigator.pop(ctx, v.trim());
}
},
decoration: const InputDecoration(hintText: 'userId'),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Cancel'),
),
TextButton(
onPressed: okEnabled()
? () => Navigator.pop(ctx, v.trim())
: null,
child: const Text('OK'),
),
],
),
);
},
).then((userId) {
if (userId != null && userId.isNotEmpty) {
_plugin.signIn(userId: userId);
}
});
}
Future<void> _showSetUserPropertiesDialog() async {
final dialogContext = _navigatorKey.currentState!.overlay!.context;
final phoneCtrl = TextEditingController();
final emailCtrl = TextEditingController();
bool? c, sms, email, push, kakao;
bool? triNext(bool? v) => v == null ? true : (v ? false : null);
Widget triButton({
required String keyName,
required bool? value,
required VoidCallback onPressed,
}) {
final label = value == null
? keyName
: '$keyName: ${value ? 'true' : 'false'}';
return value == null
? OutlinedButton(onPressed: onPressed, child: Text(label)) // 무색
: ElevatedButton(onPressed: onPressed, child: Text(label)); // 유색
}
final props = await showDialog<UserProperties>(
context: dialogContext,
builder: (ctx) => StatefulBuilder(
builder: (ctx, setState) {
bool hasAny() =>
phoneCtrl.text.trim().isNotEmpty ||
emailCtrl.text.trim().isNotEmpty ||
[c, sms, email, push, kakao].any((v) => v != null);
return AlertDialog(
title: const Text('Set UserProperties'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: phoneCtrl,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
labelText: 'phoneNumber',
hintText: '01012345678',
),
),
const SizedBox(height: 8),
TextField(
controller: emailCtrl,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
labelText: 'emailAddress',
hintText: '[email protected]',
),
),
const SizedBox(height: 12),
triButton(
keyName: 'marketing_notification_consent',
value: c,
onPressed: () => setState(() => c = triNext(c)),
),
triButton(
keyName: 'marketing_notification_sms_consent',
value: sms,
onPressed: () => setState(() => sms = triNext(sms)),
),
triButton(
keyName: 'marketing_notification_email_consent',
value: email,
onPressed: () => setState(() => email = triNext(email)),
),
triButton(
keyName: 'marketing_notification_push_consent',
value: push,
onPressed: () => setState(() => push = triNext(push)),
),
triButton(
keyName: 'marketing_notification_kakao_consent',
value: kakao,
onPressed: () => setState(() => kakao = triNext(kakao)),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Cancel'),
),
TextButton(
onPressed: hasAny()
? () {
Navigator.pop(
ctx,
UserProperties(
phoneNumber: phoneCtrl.text.trim().isEmpty
? null
: phoneCtrl.text.trim(),
emailAddress: emailCtrl.text.trim().isEmpty
? null
: emailCtrl.text.trim(),
marketingNotificationConsent: c,
marketingNotificationSmsConsent: sms,
marketingNotificationEmailConsent: email,
marketingNotificationPushConsent: push,
marketingNotificationKakaoConsent: kakao,
),
);
}
: null,
child: const Text('OK'),
),
],
);
},
),
);
if (props != null) {
await _plugin.setUserProperties(props);
}
}
Future<void> _showSetCustomUserPropertiesDialog() async {
final dialogContext = _navigatorKey.currentState!.overlay!.context;
final stringRows = <_PropRow>[_PropRow()];
final numberRows = <_PropRow>[_PropRow()];
final boolRows = <_PropRow>[_PropRow()];
bool hasAnyKey() =>
stringRows.any((e) => e.key.trim().isNotEmpty) ||
numberRows.any((e) => e.key.trim().isNotEmpty) ||
boolRows.any((e) => e.key.trim().isNotEmpty);
Map<String, dynamic> buildMap() {
final m = <String, dynamic>{};
for (final r in stringRows) {
final k = r.key.trim();
if (k.isEmpty) continue;
m[k] = r.stringValue;
}
for (final r in numberRows) {
final k = r.key.trim();
if (k.isEmpty) continue;
final t = r.numberValueText.trim();
if (t.isEmpty) continue;
final n = double.tryParse(t);
if (n == null) continue;
m[k] = n;
}
for (final r in boolRows) {
final k = r.key.trim();
if (k.isEmpty) continue;
m[k] = r.boolValue;
}
return m;
}
final props = await showDialog<Map<String, dynamic>>(
context: dialogContext,
builder: (ctx) => StatefulBuilder(
builder: (ctx, setState) {
void close([Map<String, dynamic>? value]) {
FocusScope.of(ctx).unfocus();
Navigator.pop(ctx, value);
}
Widget sectionHeader(String title) => Padding(
padding: const EdgeInsets.only(top: 12, bottom: 8),
child: Align(
alignment: Alignment.centerLeft,
child: Text(title, style: Theme.of(ctx).textTheme.titleMedium),
),
);
Widget addButton(VoidCallback onAdd) => Align(
alignment: Alignment.centerLeft,
child: OutlinedButton.icon(
onPressed: onAdd,
icon: const Icon(Icons.add),
label: const Text('Add'),
),
);
Widget removeButton({
required bool enabled,
required VoidCallback onRemove,
}) => IconButton(
tooltip: 'Remove',
onPressed: enabled ? onRemove : null,
icon: const Icon(Icons.remove_circle_outline),
);
Widget stringRowItem(int index) {
final r = stringRows[index];
return Row(
children: [
Expanded(
child: TextFormField(
key: ValueKey('s_k_${r.id}'),
initialValue: r.key,
onChanged: (v) => setState(() => r.key = v),
decoration: const InputDecoration(
labelText: 'key',
hintText: 'nickname',
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextFormField(
key: ValueKey('s_v_${r.id}'),
initialValue: r.stringValue,
onChanged: (v) => setState(() => r.stringValue = v),
decoration: const InputDecoration(
labelText: 'value',
hintText: 'Cristiano',
),
),
),
const SizedBox(width: 8),
removeButton(
enabled: stringRows.length > 1,
onRemove: () => setState(() => stringRows.removeAt(index)),
),
],
);
}
final numberFmt = FilteringTextInputFormatter.allow(
RegExp(r'^\d*\.?\d*$'), // 숫자 + 소수점 1개
);
Widget numberRowItem(int index) {
final r = numberRows[index];
return Row(
children: [
Expanded(
child: TextFormField(
key: ValueKey('n_k_${r.id}'),
initialValue: r.key,
onChanged: (v) => setState(() => r.key = v),
decoration: const InputDecoration(
labelText: 'key',
hintText: 'height',
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextFormField(
key: ValueKey('n_v_${r.id}'),
initialValue: r.numberValueText,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: false,
),
inputFormatters: [numberFmt],
onChanged: (v) => setState(() => r.numberValueText = v),
decoration: const InputDecoration(
labelText: 'value',
hintText: '177.7',
),
),
),
const SizedBox(width: 8),
removeButton(
enabled: numberRows.length > 1,
onRemove: () => setState(() => numberRows.removeAt(index)),
),
],
);
}
Widget boolRowItem(int index) {
final r = boolRows[index];
return Row(
children: [
Expanded(
child: TextFormField(
key: ValueKey('b_k_${r.id}'),
initialValue: r.key,
onChanged: (v) => setState(() => r.key = v),
decoration: const InputDecoration(
labelText: 'key',
hintText: 'is_active',
),
),
),
const SizedBox(width: 8),
Expanded(
child: DropdownButtonFormField<bool>(
key: ValueKey('b_v_${r.id}'),
value: r.boolValue,
items: const [
DropdownMenuItem(value: true, child: Text('true')),
DropdownMenuItem(value: false, child: Text('false')),
],
onChanged: (v) => setState(() => r.boolValue = v ?? true),
decoration: const InputDecoration(labelText: 'value'),
),
),
const SizedBox(width: 8),
removeButton(
enabled: boolRows.length > 1,
onRemove: () => setState(() => boolRows.removeAt(index)),
),
],
);
}
return AlertDialog(
title: const Text('Set CustomUserProperties'),
content: SizedBox(
width: 560,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
sectionHeader('String'),
...List.generate(stringRows.length, stringRowItem),
const SizedBox(height: 8),
addButton(() => setState(() => stringRows.add(_PropRow()))),
sectionHeader('Number'),
...List.generate(numberRows.length, numberRowItem),
const SizedBox(height: 8),
addButton(() => setState(() => numberRows.add(_PropRow()))),
sectionHeader('Boolean'),
...List.generate(boolRows.length, boolRowItem),
const SizedBox(height: 8),
addButton(() => setState(() => boolRows.add(_PropRow()))),
],
),
),
),
actions: [
TextButton(onPressed: () => close(), child: const Text('Cancel')),
TextButton(
onPressed: hasAnyKey() ? () => close(buildMap()) : null,
child: const Text('OK'),
),
],
);
},
),
);
if (props != null && props.isNotEmpty) {
await _plugin.setCustomUserProperties(props);
}
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
}
class _PropRow {
final String id = UniqueKey().toString();
String key = '';
String stringValue = '';
String numberValueText = '';
bool boolValue = true;
_PropRow();
}