blux_flutter 0.1.2 copy "blux_flutter: ^0.1.2" to clipboard
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&param2=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();
}