blux_flutter 0.1.6
blux_flutter: ^0.1.6 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/notifications/inapp_custom_action_event.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);
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>();
APIStage _currentStage = APIStage.stg;
/// Custom HTML 인앱 핸들러 해제 함수
void Function()? _inAppCustomActionUnsubscribe;
// Custom Event 입력 컨트롤러
final _eventTypeController = TextEditingController();
final _propKeyController = TextEditingController();
final _propValueController = TextEditingController();
// 상태 추적
bool _isInitialized = false;
String? _signedInUserId;
// Prod credentials
static const _prodApplicationId = '693271b484763c6fb1a85054';
static const _prodApiKey = 'CDtIFIKIk02Dm3jIFMKQXmGvKRzDhQcUOGN_XUso';
// Stg credentials
static const _stgApplicationId = '69327603beb1da48e4278eca';
static const _stgApiKey = 'EPaHV6oaJZmbwpvY_i6EmHrw8Sq5KhE0iaOZ7ICE';
@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) {
final isProd = _currentStage == APIStage.prod;
final currentAppId = isProd ? _prodApplicationId : _stgApplicationId;
return MaterialApp(
navigatorKey: _navigatorKey,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Blux SDK for Flutter'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Status 영역
_buildStatusCard(currentAppId),
const SizedBox(height: 16),
// Stage 선택
_buildSectionCard(
title: 'Stage',
child: Row(
children: [
Expanded(
child: _buildStageButton('Production', APIStage.prod),
),
const SizedBox(width: 8),
Expanded(
child: _buildStageButton('Staging', APIStage.stg),
),
],
),
),
const SizedBox(height: 12),
// User 섹션
_buildSectionCard(
title: 'User',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildPrimaryButton(
label: 'Initialize',
icon: Icons.play_arrow,
onPressed: _handleInitialize,
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: _buildSecondaryButton(
label: 'Sign In',
onPressed: _showSignInDialog,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildSecondaryButton(
label: 'Sign Out',
onPressed: _handleSignOut,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildSecondaryButton(
label: 'User Props',
onPressed: _showSetUserPropertiesDialog,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildSecondaryButton(
label: 'Custom Props',
onPressed: _showSetCustomUserPropertiesDialog,
),
),
],
),
],
),
),
const SizedBox(height: 12),
// Events 섹션
_buildSectionCard(
title: 'Events',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Custom Event 입력 필드
TextField(
controller: _eventTypeController,
decoration: InputDecoration(
labelText: 'Event Type',
hintText: 'custom_event',
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
),
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: TextField(
controller: _propKeyController,
decoration: InputDecoration(
labelText: 'Prop Key',
hintText: 'key',
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _propValueController,
decoration: InputDecoration(
labelText: 'Prop Value',
hintText: 'value',
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
),
),
),
],
),
const SizedBox(height: 10),
_buildPrimaryButton(
label: 'Send Custom Event',
icon: Icons.send,
onPressed: _sendCustomEvent,
),
const SizedBox(height: 12),
const Divider(),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildEventChip('PDV', () {
_plugin.sendEvent(AddProductDetailViewEvent(itemId: '1234'));
_showToast('ProductDetailView Sent');
}),
_buildEventChip('CartAdd', () {
_plugin.sendEvent(AddCartaddEvent(itemId: '1234'));
_showToast('CartAdd Sent');
}),
_buildEventChip('Order', () {
_plugin.sendEvent(AddOrderEvent(
orderAmount: 200,
paidAmount: 100,
items: [{'id': '1234', 'price': 200, 'quantity': 1}],
orderId: 'order_${DateTime.now().millisecondsSinceEpoch}',
));
_showToast('Order Sent');
}),
],
),
],
),
),
const SizedBox(height: 12),
// WebView 섹션
_buildSectionCard(
title: 'WebView',
child: Row(
children: [
Expanded(
child: Builder(
builder: (ctx) => _buildSecondaryButton(
label: 'webview_flutter',
onPressed: () => Navigator.push(
ctx,
MaterialPageRoute(
builder: (_) => BluxWebView(initialUrl: _buildWebViewUrl()),
),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Builder(
builder: (ctx) => _buildSecondaryButton(
label: 'inappwebview',
onPressed: () => Navigator.push(
ctx,
MaterialPageRoute(
builder: (_) => BluxInAppWebView(initialUrl: _buildWebViewUrl()),
),
),
),
),
),
],
),
),
const SizedBox(height: 12),
],
),
),
),
);
}
Widget _buildStatusCard(String appId) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _isInitialized ? Colors.green : Colors.grey.shade400,
),
),
const SizedBox(width: 8),
Text(
_isInitialized ? 'Initialized' : 'Not Initialized',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
color: _isInitialized ? Colors.green.shade700 : Colors.grey.shade600,
),
),
],
),
const SizedBox(height: 12),
_buildStatusRow('Platform', _platformVersion),
_buildStatusRow('Stage', _currentStage.name.toUpperCase()),
_buildStatusRow('App ID', appId),
_buildStatusRow('User ID', _signedInUserId ?? '(not signed in)'),
],
),
);
}
Widget _buildStatusRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
SizedBox(
width: 70,
child: Text(
'$label:',
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _buildSectionCard({required String title, required Widget child}) {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
),
const SizedBox(height: 14),
child,
],
),
),
);
}
Widget _buildStageButton(String label, APIStage stage) {
final isSelected = _currentStage == stage;
return GestureDetector(
onTap: () {
setState(() => _currentStage = stage);
_plugin.setAPIStage(stage);
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.grey.shade200,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : Colors.black87,
fontWeight: FontWeight.w500,
),
),
),
),
);
}
Widget _buildPrimaryButton({
required String label,
required VoidCallback onPressed,
IconData? icon,
}) {
return GestureDetector(
onTap: onPressed,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, color: Colors.white, size: 20),
const SizedBox(width: 8),
],
Text(
label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
Widget _buildSecondaryButton({
required String label,
required VoidCallback onPressed,
}) {
return GestureDetector(
onTap: onPressed,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade300),
),
child: Center(
child: Text(
label,
style: TextStyle(
color: Colors.grey.shade700,
fontWeight: FontWeight.w500,
),
),
),
),
);
}
Widget _buildEventChip(String label, VoidCallback onPressed) {
return GestureDetector(
onTap: onPressed,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.blue.shade200),
),
child: Text(
label,
style: TextStyle(
color: Colors.blue.shade700,
fontWeight: FontWeight.w500,
),
),
),
);
}
Future<void> _handleInitialize() async {
final isProd = _currentStage == APIStage.prod;
await _plugin.initialize(
bluxApplicationId: isProd ? _prodApplicationId : _stgApplicationId,
bluxAPIKey: isProd ? _prodApiKey : _stgApiKey,
requestPermissionsOnLaunch: true,
);
_inAppCustomActionUnsubscribe = await _plugin.addInAppCustomActionHandler(
_handleInAppCustomAction,
);
setState(() => _isInitialized = true);
_showToast('Initialized');
}
void _handleSignOut() {
_plugin.signOut();
setState(() => _signedInUserId = null);
_showToast('Signed Out');
}
void _showToast(String message) {
ScaffoldMessenger.of(_navigatorKey.currentContext!).showSnackBar(
SnackBar(content: Text(message), duration: const Duration(seconds: 1)),
);
}
String _buildWebViewUrl() {
final isProd = _currentStage == APIStage.prod;
final stage = isProd ? 'prod' : 'stg';
final applicationId = isProd ? _prodApplicationId : _stgApplicationId;
final apiKey = isProd ? _prodApiKey : _stgApiKey;
return 'https://stg.sdk-demo.blux.ai/?application_id=$applicationId&api_key=$apiKey&stage=$stage&platform=flutter';
}
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);
setState(() => _signedInUserId = userId);
_showToast('Signed In: $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: 'test@example.com',
),
),
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);
}
}
/// Custom HTML 인앱 메시지에서 BluxBridge.triggerAction() 호출 시 실행
void _handleInAppCustomAction(InAppCustomActionEvent event) {
debugPrint('InAppCustomAction received:');
debugPrint(' actionId: ${event.actionId}');
debugPrint(' data: ${event.data}');
// data를 문자열로 변환
final dataString = event.data.entries
.map((e) => '${e.key}: ${e.value}')
.join(', ');
final message = 'actionId: ${event.actionId}\ndata: {$dataString}';
// dismiss 애니메이션 완료 후 alert 표시 (300ms 지연)
Future.delayed(const Duration(milliseconds: 300), () {
final context = _navigatorKey.currentState?.overlay?.context;
if (context == null) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Custom Action'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
});
}
void _sendCustomEvent() {
final eventType = _eventTypeController.text.trim();
if (eventType.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Event Type is required')),
);
return;
}
final propKey = _propKeyController.text.trim();
final propValue = _propValueController.text.trim();
Map<String, dynamic>? customProps;
if (propKey.isNotEmpty && propValue.isNotEmpty) {
customProps = {propKey: propValue};
}
_plugin.sendEvent(
AddCustomEvent(
eventType: eventType,
customEventProperties: customProps,
),
);
debugPrint('Custom Event Sent: $eventType');
// 키보드 숨기기 & 토스트 표시
final messenger = ScaffoldMessenger.of(context);
FocusScope.of(context).unfocus();
messenger.showSnackBar(
SnackBar(content: Text('Custom Event Sent: $eventType')),
);
}
@override
void dispose() {
_sub?.cancel();
_inAppCustomActionUnsubscribe?.call();
_eventTypeController.dispose();
_propKeyController.dispose();
_propValueController.dispose();
super.dispose();
}
}
class _PropRow {
final String id = UniqueKey().toString();
String key = '';
String stringValue = '';
String numberValueText = '';
bool boolValue = true;
_PropRow();
}